Files
runner/.opencode/plans/dap-step-manipulation.md
Francesco Renzi 008594a3ee editing jobs
2026-01-21 23:19:25 +00:00

32 KiB

Dynamic Step Manipulation & Workflow Export

Status: Draft
Author: GitHub Actions Team
Date: January 2026
Prerequisites: dap-debugging.md, dap-step-backwards.md (completed)

Progress Checklist

  • Chunk 1: Command Parser & Infrastructure
  • Chunk 2: Step Serializer (ActionStep → YAML)
  • Chunk 3: Step Factory (Create new steps)
  • Chunk 4: Step Manipulator (Queue operations)
  • Chunk 5: REPL Commands (!step list, !step add run, !step edit, !step remove, !step move)
  • Chunk 6: Action Download Integration (!step add uses)
  • Chunk 7: Export Command (!step export)
  • Chunk 8: JSON API for Browser Extension
  • Chunk 9: Browser Extension UI

Overview

This plan extends the DAP debugger with the ability to dynamically manipulate job steps during a debug session: add new steps, edit pending steps, remove steps, and reorder them. At the end of a session, users can export the modified steps as YAML to paste into their workflow file.

This transforms the debugger from a "read-only inspection tool" into an interactive workflow editor — the key differentiator of this prototype.

Goals

  • Primary: Enable add/edit/move/delete of job steps during debug session
  • Primary: Support both run and uses step types
  • Primary: Export modified steps as valid YAML
  • Secondary: Provide both REPL commands and JSON API for different clients
  • Non-goal: Full workflow file reconstruction (steps section only)
  • Non-goal: Production action restriction enforcement (noted for later)

Command API Specification

Grammar

!step <command> [target] [options]

Index Reference

  • 1-based indexing for user-friendliness
  • Completed steps are shown but read-only
  • Cannot modify currently executing step (except via step-back)

Commands Summary

Command Purpose Example
!step list Show all steps !step list --verbose
!step add Add new step !step add run "npm test" --after 3
!step edit Modify step !step edit 4 --script "npm run test:ci"
!step remove Delete step !step remove 5
!step move Reorder step !step move 5 --after 2
!step export Generate YAML !step export --with-comments

Position Modifiers

For !step add and !step move:

  • --at <index> — Insert at specific position
  • --after <index> — Insert after step
  • --before <index> — Insert before step
  • --first — Insert at first pending position
  • --last — Insert at end (default)

Full Command Reference

See "Command API Full Reference" section at end of document.


Implementation Chunks

Chunk 1: Command Parser & Infrastructure

Goal: Create the foundation for parsing and dispatching step commands.

Files to create:

  • src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs
  • src/Runner.Worker/Dap/StepCommands/StepCommandResult.cs

Files to modify:

  • src/Runner.Worker/Dap/DapDebugSession.cs — Add command dispatch in HandleEvaluate()

Details:

  1. StepCommandParser — Parse REPL command strings into structured commands:

    public interface IStepCommandParser
    {
        StepCommand Parse(string input);  // "!step add run \"echo hello\" --after 3"
        bool IsStepCommand(string input); // Starts with "!step" or is JSON with cmd:"step.*"
    }
    
    public abstract class StepCommand { }
    public class ListCommand : StepCommand { public bool Verbose; }
    public class AddRunCommand : StepCommand { 
        public string Script; 
        public string Name;
        public string Shell;
        public StepPosition Position;
        // ...
    }
    public class AddUsesCommand : StepCommand { /* ... */ }
    public class EditCommand : StepCommand { /* ... */ }
    public class RemoveCommand : StepCommand { public int Index; }
    public class MoveCommand : StepCommand { public int FromIndex; public StepPosition Position; }
    public class ExportCommand : StepCommand { public bool ChangesOnly; public bool WithComments; }
    
  2. StepPosition — Represent insertion position:

    public class StepPosition
    {
        public PositionType Type { get; set; }  // At, After, Before, First, Last
        public int? Index { get; set; }         // For At, After, Before
    }
    
  3. StepCommandResult — Standardized response:

    public class StepCommandResult
    {
        public bool Success { get; set; }
        public string Message { get; set; }
        public string Error { get; set; }      // Error code
        public object Result { get; set; }     // Command-specific data
    }
    
  4. Integration in DapDebugSession.HandleEvaluate():

    // After checking for !debug command
    if (_stepCommandParser.IsStepCommand(expression))
    {
        return await HandleStepCommandAsync(expression, executionContext);
    }
    

Testing:

  • Unit tests for command parsing
  • Test various edge cases (quoted strings, escapes, missing args)

Estimated effort: Small-medium


Chunk 2: Step Serializer (ActionStep → YAML)

Goal: Convert Pipelines.ActionStep objects to YAML string representation.

Files to create:

  • src/Runner.Worker/Dap/StepCommands/StepSerializer.cs

Details:

  1. IStepSerializer interface:

    public interface IStepSerializer
    {
        string ToYaml(Pipelines.ActionStep step);
        string ToYaml(IEnumerable<StepInfo> steps, bool withComments = false);
    }
    
  2. Handle both step types:

    For run steps (ScriptReference):

    - name: Run Tests
      run: |
        npm ci
        npm test
      shell: bash
      working-directory: src
      env:
        NODE_ENV: test
      if: success()
      continue-on-error: false
      timeout-minutes: 10
    

    For uses steps (RepositoryPathReference):

    - name: Setup Node
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: npm
      env:
        NODE_OPTIONS: --max-old-space-size=4096
      if: success()
    
  3. Extract data from ActionStep:

    • Reference type determines run vs uses
    • Inputs TemplateToken contains script (for run) or with values (for uses)
    • Environment TemplateToken contains env vars
    • Condition string contains if expression
    • DisplayName or DisplayNameToken for name
  4. Use existing YAML infrastructure:

    • Consider using YamlObjectWriter from WorkflowParser
    • Or use a simple string builder for more control

Testing:

  • Round-trip tests: create step → serialize → verify YAML
  • Test all step properties
  • Test multi-line scripts

Estimated effort: Medium


Chunk 3: Step Factory (Create new steps)

Goal: Create Pipelines.ActionStep and IActionRunner objects at runtime.

Files to create:

  • src/Runner.Worker/Dap/StepCommands/StepFactory.cs

Details:

  1. IStepFactory interface:

    public interface IStepFactory : IRunnerService
    {
        Pipelines.ActionStep CreateRunStep(
            string script,
            string name = null,
            string shell = null,
            string workingDirectory = null,
            Dictionary<string, string> env = null,
            string condition = null,
            bool continueOnError = false,
            int? timeoutMinutes = null);
    
        Pipelines.ActionStep CreateUsesStep(
            string actionReference,  // "owner/repo@ref"
            string name = null,
            Dictionary<string, string> with = null,
            Dictionary<string, string> env = null,
            string condition = null,
            bool continueOnError = false,
            int? timeoutMinutes = null);
    
        IActionRunner WrapInRunner(
            Pipelines.ActionStep step,
            IExecutionContext jobContext,
            ActionRunStage stage = ActionRunStage.Main);
    }
    
  2. CreateRunStep implementation:

    public Pipelines.ActionStep CreateRunStep(...)
    {
        var step = new Pipelines.ActionStep
        {
            Id = Guid.NewGuid(),
            Name = $"_dynamic_{Guid.NewGuid():N}",
            DisplayName = name ?? "Run script",
            Reference = new ScriptReference(),
            Condition = condition ?? "success()",
            ContinueOnError = CreateBoolToken(continueOnError),
            TimeoutInMinutes = CreateIntToken(timeoutMinutes)
        };
    
        // Build Inputs mapping with script, shell, working-directory
        step.Inputs = CreateRunInputs(script, shell, workingDirectory);
    
        // Build Environment mapping
        if (env?.Count > 0)
            step.Environment = CreateEnvToken(env);
    
        return step;
    }
    
  3. CreateUsesStep implementation:

    public Pipelines.ActionStep CreateUsesStep(string actionReference, ...)
    {
        var (name, ref_, path) = ParseActionReference(actionReference);
    
        var step = new Pipelines.ActionStep
        {
            Id = Guid.NewGuid(),
            Name = $"_dynamic_{Guid.NewGuid():N}",
            DisplayName = displayName ?? actionReference,
            Reference = new RepositoryPathReference
            {
                Name = name,        // "actions/checkout"
                Ref = ref_,         // "v4"
                Path = path,        // null or "subdir"
                RepositoryType = "GitHub"
            },
            Condition = condition ?? "success()"
        };
    
        // Build with inputs
        if (with?.Count > 0)
            step.Inputs = CreateWithInputs(with);
    
        return step;
    }
    
  4. ParseActionReference helper:

    • actions/checkout@v4 → name=actions/checkout, ref=v4, path=null
    • actions/setup-node@v4 → name=actions/setup-node, ref=v4
    • owner/repo/subdir@ref → name=owner/repo, ref=ref, path=subdir
    • docker://alpine:latest → ContainerRegistryReference instead
  5. WrapInRunner: Create IActionRunner from ActionStep (copy pattern from JobExtension.cs):

    public IActionRunner WrapInRunner(Pipelines.ActionStep step, IExecutionContext jobContext, ActionRunStage stage)
    {
        var runner = HostContext.CreateService<IActionRunner>();
        runner.Action = step;
        runner.Stage = stage;
        runner.Condition = step.Condition;
        runner.ExecutionContext = jobContext.CreateChild(
            Guid.NewGuid(),
            step.DisplayName,
            step.Name
        );
        return runner;
    }
    

Testing:

  • Create run step → verify all properties
  • Create uses step → verify reference parsing
  • Test edge cases (missing shell, local actions, docker actions)

Estimated effort: Medium


Chunk 4: Step Manipulator (Queue operations)

Goal: Manipulate the job step queue (add, remove, move, track changes).

Files to create:

  • src/Runner.Worker/Dap/StepCommands/StepManipulator.cs
  • src/Runner.Worker/Dap/StepCommands/StepInfo.cs
  • src/Runner.Worker/Dap/StepCommands/StepChange.cs

Details:

  1. StepInfo — Unified step representation:

    public class StepInfo
    {
        public int Index { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }       // "run" or "uses"
        public string TypeDetail { get; set; } // action ref or script preview
        public StepStatus Status { get; set; } // Completed, Current, Pending
        public ChangeType? Change { get; set; } // Added, Modified, null
        public Pipelines.ActionStep Action { get; set; }
        public IStep Step { get; set; }
    }
    
    public enum StepStatus { Completed, Current, Pending }
    public enum ChangeType { Added, Modified, Removed, Moved }
    
  2. StepChange — Track modifications:

    public class StepChange
    {
        public ChangeType Type { get; set; }
        public int OriginalIndex { get; set; }
        public StepInfo OriginalStep { get; set; }
        public StepInfo ModifiedStep { get; set; }
    }
    
  3. IStepManipulator interface:

    public interface IStepManipulator : IRunnerService
    {
        // Initialize with job context
        void Initialize(IExecutionContext jobContext, int currentStepIndex);
        void UpdateCurrentIndex(int index);
    
        // Query
        IReadOnlyList<StepInfo> GetAllSteps();
        StepInfo GetStep(int index);
        int GetPendingCount();
        int GetFirstPendingIndex();
    
        // Mutate
        int InsertStep(IStep step, StepPosition position);
        void RemoveStep(int index);
        void MoveStep(int fromIndex, StepPosition position);
        void EditStep(int index, Action<Pipelines.ActionStep> edit);
    
        // Change tracking
        IReadOnlyList<StepChange> GetChanges();
        void RecordOriginalState();  // Call at session start
    }
    
  4. Implementation details:

    • Maintain list of completed steps (from checkpoints/history)
    • Access jobContext.JobSteps queue for pending steps
    • Track all modifications for export diff
    • Validate indices before operations
  5. Queue manipulation:

    public int InsertStep(IStep step, StepPosition position)
    {
        // Convert queue to list
        var pending = _jobContext.JobSteps.ToList();
        _jobContext.JobSteps.Clear();
    
        // Calculate insertion index
        int insertAt = CalculateInsertIndex(position, pending.Count);
    
        // Insert
        pending.Insert(insertAt, step);
    
        // Re-queue
        foreach (var s in pending)
            _jobContext.JobSteps.Enqueue(s);
    
        // Track change
        _changes.Add(new StepChange { Type = ChangeType.Added, ... });
    
        return _currentIndex + insertAt + 1; // Return 1-based index
    }
    

Testing:

  • Insert at various positions
  • Remove and verify queue state
  • Move operations
  • Change tracking accuracy

Estimated effort: Medium


Chunk 5: REPL Commands (run steps)

Goal: Implement !step list, !step add run, !step edit, !step remove, !step move.

Files to modify:

  • src/Runner.Worker/Dap/DapDebugSession.cs — Add HandleStepCommandAsync()

Files to create:

  • src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs

Details:

  1. IStepCommandHandler interface:

    public interface IStepCommandHandler : IRunnerService
    {
        Task<StepCommandResult> HandleAsync(StepCommand command, IExecutionContext context);
    }
    
  2. Command implementations:

    List:

    case ListCommand list:
        var steps = _manipulator.GetAllSteps();
        var output = FormatStepList(steps, list.Verbose);
        return new StepCommandResult { Success = true, Result = steps, Message = output };
    

    Add run:

    case AddRunCommand add:
        var actionStep = _factory.CreateRunStep(
            add.Script, add.Name, add.Shell, add.WorkingDirectory,
            add.Env, add.Condition, add.ContinueOnError, add.Timeout);
        var runner = _factory.WrapInRunner(actionStep, context);
        var index = _manipulator.InsertStep(runner, add.Position);
        return new StepCommandResult { 
            Success = true, 
            Message = $"Step added at position {index}",
            Result = new { index, step = GetStepInfo(runner) }
        };
    

    Edit:

    case EditCommand edit:
        ValidatePendingIndex(edit.Index);
        _manipulator.EditStep(edit.Index, step => {
            if (edit.Script != null) UpdateScript(step, edit.Script);
            if (edit.Name != null) step.DisplayName = edit.Name;
            if (edit.Condition != null) step.Condition = edit.Condition;
            // ... other fields
        });
        return new StepCommandResult { Success = true, Message = $"Step {edit.Index} updated" };
    

    Remove:

    case RemoveCommand remove:
        ValidatePendingIndex(remove.Index);
        _manipulator.RemoveStep(remove.Index);
        return new StepCommandResult { Success = true, Message = $"Step {remove.Index} removed" };
    

    Move:

    case MoveCommand move:
        ValidatePendingIndex(move.FromIndex);
        var newIndex = _manipulator.MoveStep(move.FromIndex, move.Position);
        return new StepCommandResult { Success = true, Message = $"Step moved to position {newIndex}" };
    
  3. Integration in DapDebugSession:

    private async Task<Response> HandleStepCommandAsync(string expression, IExecutionContext context)
    {
        try
        {
            var command = _commandParser.Parse(expression);
            var result = await _commandHandler.HandleAsync(command, context);
    
            if (result.Success)
                return CreateSuccessResponse(new EvaluateResponseBody
                {
                    Result = result.Message,
                    Type = "string"
                });
            else
                return CreateErrorResponse($"{result.Error}: {result.Message}");
        }
        catch (StepCommandException ex)
        {
            return CreateErrorResponse(ex.Message);
        }
    }
    

Testing:

  • End-to-end: add step → list → verify
  • Edit various properties
  • Remove and verify indices shift
  • Move operations

Estimated effort: Medium-large


Chunk 6: Action Download Integration (!step add uses)

Goal: Support !step add uses with full action download.

Files to modify:

  • src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs

Details:

  1. Add uses command handling:

    case AddUsesCommand add:
        // Create the step
        var actionStep = _factory.CreateUsesStep(
            add.Action, add.Name, add.With, add.Env, 
            add.Condition, add.ContinueOnError, add.Timeout);
    
        // Download the action (this is the key difference from run steps)
        var actionManager = HostContext.GetService<IActionManager>();
        var prepareResult = await actionManager.PrepareActionsAsync(
            context, 
            new[] { actionStep }
        );
    
        // Check for pre-steps (some actions have setup steps)
        if (prepareResult.PreStepTracker.TryGetValue(actionStep.Id, out var preStep))
        {
            // Insert pre-step before main step
            var preRunner = _factory.WrapInRunner(preStep.Action, context, ActionRunStage.Pre);
            _manipulator.InsertStep(preRunner, add.Position);
        }
    
        // Wrap and insert main step
        var runner = _factory.WrapInRunner(actionStep, context);
        var index = _manipulator.InsertStep(runner, CalculateMainPosition(add.Position, hasPreStep));
    
        // Handle post-steps (cleanup)
        // These go to PostJobSteps stack, handled by existing infrastructure
    
        return new StepCommandResult { 
            Success = true, 
            Message = $"Action '{add.Action}' added at position {index}",
            Result = new { index, step = GetStepInfo(runner) }
        };
    
  2. Error handling:

    • Action not found → clear error message with action name
    • Network failure → suggest retry
    • Invalid action reference format → parse error
  3. Production note (for future):

    // TODO: Before production release, add action restriction checks:
    // - Verify action is in organization's allowed list
    // - Check verified creator requirements
    // - Enforce enterprise policies
    // For now, allow all actions in prototype
    

Testing:

  • Add common actions (checkout, setup-node)
  • Test actions with pre/post steps
  • Test local actions (./.github/actions/...)
  • Test docker actions (docker://...)
  • Error cases: invalid action, network failure

Estimated effort: Medium


Chunk 7: Export Command (!step export)

Goal: Generate YAML output for modified steps.

Files to modify:

  • src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs

Details:

  1. Export command handling:

    case ExportCommand export:
        var steps = _manipulator.GetAllSteps();
        var changes = _manipulator.GetChanges();
    
        IEnumerable<StepInfo> toExport;
        if (export.ChangesOnly)
        {
            toExport = steps.Where(s => s.Change != null);
        }
        else
        {
            toExport = steps;
        }
    
        var yaml = _serializer.ToYaml(toExport, export.WithComments);
    
        return new StepCommandResult
        {
            Success = true,
            Message = yaml,
            Result = new { 
                yaml, 
                totalSteps = steps.Count,
                addedCount = changes.Count(c => c.Type == ChangeType.Added),
                modifiedCount = changes.Count(c => c.Type == ChangeType.Modified)
            }
        };
    
  2. YAML output format:

    steps:
      - name: Checkout
        uses: actions/checkout@v4
    
      - name: Setup Node  # ADDED
        uses: actions/setup-node@v4
        with:
          node-version: '20'
    
      - name: Run Tests  # MODIFIED
        run: |
          npm ci
          npm test
        shell: bash
    
  3. Change comments (when --with-comments):

    • # ADDED for new steps
    • # MODIFIED for edited steps
    • Removed steps listed at bottom as comments (optional)

Testing:

  • Export with no changes → valid YAML
  • Export with additions → ADDED comments
  • Export with modifications → MODIFIED comments
  • Verify YAML is valid and can be pasted into workflow

Estimated effort: Small


Chunk 8: JSON API for Browser Extension

Goal: Add JSON command support for programmatic access.

Files to modify:

  • src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs
  • src/Runner.Worker/Dap/DapDebugSession.cs

Details:

  1. Detect JSON input:

    public bool IsStepCommand(string input)
    {
        var trimmed = input.Trim();
        return trimmed.StartsWith("!step") || 
               (trimmed.StartsWith("{") && trimmed.Contains("\"cmd\"") && trimmed.Contains("\"step."));
    }
    
  2. Parse JSON commands:

    public StepCommand Parse(string input)
    {
        var trimmed = input.Trim();
        if (trimmed.StartsWith("{"))
            return ParseJsonCommand(trimmed);
        else
            return ParseReplCommand(trimmed);
    }
    
    private StepCommand ParseJsonCommand(string json)
    {
        var obj = JObject.Parse(json);
        var cmd = obj["cmd"]?.ToString();
    
        return cmd switch
        {
            "step.list" => new ListCommand { Verbose = obj["verbose"]?.Value<bool>() ?? false },
            "step.add" => ParseJsonAddCommand(obj),
            "step.edit" => ParseJsonEditCommand(obj),
            "step.remove" => new RemoveCommand { Index = obj["index"].Value<int>() },
            "step.move" => ParseJsonMoveCommand(obj),
            "step.export" => new ExportCommand { 
                ChangesOnly = obj["changesOnly"]?.Value<bool>() ?? false,
                WithComments = obj["withComments"]?.Value<bool>() ?? false
            },
            _ => throw new StepCommandException($"Unknown command: {cmd}")
        };
    }
    
  3. JSON response format:

    // For JSON input, return structured JSON response
    if (wasJsonInput)
    {
        return CreateSuccessResponse(new EvaluateResponseBody
        {
            Result = JsonConvert.SerializeObject(result),
            Type = "json"
        });
    }
    

Testing:

  • All commands via JSON
  • Verify JSON responses are parseable
  • Test error responses

Estimated effort: Small-medium


Chunk 9: Browser Extension UI

Goal: Add step manipulation UI to the browser extension.

Files to modify:

  • browser-ext/content/content.js
  • browser-ext/content/content.css
  • browser-ext/background/background.js

Details:

  1. Steps Panel in Debugger Pane:

    <div class="dap-steps-panel">
      <div class="dap-steps-header">
        <span>Steps</span>
        <button class="dap-add-step-btn">+ Add</button>
      </div>
      <div class="dap-steps-list">
        <!-- Steps rendered here -->
      </div>
      <div class="dap-steps-footer">
        <button class="dap-export-btn">Export Changes</button>
      </div>
    </div>
    
  2. Step List Rendering:

    function renderSteps(steps) {
      return steps.map(step => `
        <div class="dap-step ${step.status}" data-index="${step.index}">
          <span class="dap-step-status">${getStatusIcon(step.status)}</span>
          <span class="dap-step-index">${step.index}.</span>
          <span class="dap-step-name">${step.name}</span>
          ${step.change ? `<span class="dap-step-badge">[${step.change}]</span>` : ''}
          <span class="dap-step-type">${step.type}</span>
          ${step.status === 'pending' ? renderStepActions(step) : ''}
        </div>
      `).join('');
    }
    
  3. Add Step Dialog:

    function showAddStepDialog() {
      // Modal with:
      // - Type selector: run / uses
      // - For run: script textarea, shell dropdown
      // - For uses: action input with autocomplete
      // - Common: name, if condition, env vars
      // - Position: dropdown (after current, at end, at position)
    }
    
  4. Step Context Menu:

    function showStepContextMenu(stepIndex, event) {
      // Edit, Move Up, Move Down, Delete
    }
    
  5. Export Modal:

    function showExportModal(yaml) {
      // Code view with syntax highlighting
      // Copy to clipboard button
    }
    
  6. Send commands via JSON API:

    async function addStep(type, options) {
      const cmd = { cmd: 'step.add', type, ...options };
      const response = await sendEvaluate(JSON.stringify(cmd));
      refreshStepList();
    }
    

Testing:

  • Manual testing in Chrome
  • All UI operations work correctly
  • Responsive layout

Estimated effort: Medium-large


File Summary

New Files

File Chunk Purpose
StepCommands/StepCommandParser.cs 1 Parse REPL and JSON commands
StepCommands/StepCommandResult.cs 1 Standardized command responses
StepCommands/StepSerializer.cs 2 ActionStep → YAML conversion
StepCommands/StepFactory.cs 3 Create new ActionStep objects
StepCommands/StepManipulator.cs 4 Queue operations & change tracking
StepCommands/StepInfo.cs 4 Step info data structure
StepCommands/StepChange.cs 4 Change tracking data structure
StepCommands/StepCommandHandler.cs 5 Command execution logic

Modified Files

File Chunk Changes
DapDebugSession.cs 1, 5 Add command dispatch, wire up services
content.js 9 Steps panel, dialogs, export modal
content.css 9 Styling for new UI elements
background.js 9 Helper functions if needed

Command API Full Reference

!step list

!step list [--verbose]

Show all steps with their indices, status, and modification state.

Output format:

Steps:
  ✓ 1. Checkout                          uses  actions/checkout@v4
  ✓ 2. Setup Node                        uses  actions/setup-node@v4
  ▶ 3. Install deps                      run   npm ci
    4. Run tests           [MODIFIED]    run   npm test
    5. Build               [ADDED]       run   npm run build
    6. Deploy                            run   ./deploy.sh

Legend: ✓ = completed, ▶ = current/paused, [ADDED] = new, [MODIFIED] = edited

JSON:

{"cmd": "step.list", "verbose": false}

!step add

Run step:

!step add run "<script>" [options]

Options:
  --name "<name>"              Display name
  --shell <shell>              Shell (bash, sh, pwsh, etc.)
  --working-directory <path>   Working directory
  --if "<condition>"           Condition expression
  --env KEY=value              Environment variable (repeatable)
  --continue-on-error          Don't fail job on step failure
  --timeout <minutes>          Step timeout
  --at <index>                 Insert at position
  --after <index>              Insert after step
  --before <index>             Insert before step
  --first                      Insert at first pending position
  --last                       Insert at end (default)

Uses step:

!step add uses <action> [options]

Options:
  --name "<name>"              Display name
  --with key=value             Input parameter (repeatable)
  --if "<condition>"           Condition expression
  --env KEY=value              Environment variable (repeatable)
  --continue-on-error          Don't fail job on step failure
  --timeout <minutes>          Step timeout
  [position options same as run]

JSON (run):

{
  "cmd": "step.add",
  "type": "run",
  "script": "npm test",
  "name": "Run Tests",
  "shell": "bash",
  "workingDirectory": "src",
  "if": "success()",
  "env": {"NODE_ENV": "test"},
  "continueOnError": false,
  "timeout": 10,
  "position": {"after": 3}
}

JSON (uses):

{
  "cmd": "step.add",
  "type": "uses",
  "action": "actions/setup-node@v4",
  "name": "Setup Node",
  "with": {"node-version": "20"},
  "env": {},
  "if": "success()",
  "position": {"at": 2}
}

Position object options:

{"at": 3}        // Insert at position 3
{"after": 2}     // Insert after step 2  
{"before": 4}    // Insert before step 4
{"first": true}  // Insert at first pending position
{"last": true}   // Insert at end (default if omitted)

!step edit

!step edit <index> [modifications]

Modifications:
  --name "<name>"              Change display name
  --script "<script>"          Change script (run only)
  --action "<action>"          Change action (uses only)
  --shell <shell>              Change shell (run only)
  --working-directory <path>   Change working directory
  --if "<condition>"           Change condition
  --with key=value             Set/update input (uses only)
  --env KEY=value              Set/update env var
  --remove-with <key>          Remove input
  --remove-env <KEY>           Remove env var
  --continue-on-error          Enable continue-on-error
  --no-continue-on-error       Disable continue-on-error
  --timeout <minutes>          Change timeout

JSON:

{
  "cmd": "step.edit",
  "index": 4,
  "script": "npm run test:ci",
  "name": "CI Tests",
  "with": {"node-version": "22"},
  "removeWith": ["cache"],
  "env": {"CI": "true"},
  "removeEnv": ["DEBUG"]
}

!step remove

!step remove <index>

JSON:

{"cmd": "step.remove", "index": 5}

!step move

!step move <from> <position>

Position (one required):
  --to <index>       Move to position
  --after <index>    Move after step
  --before <index>   Move before step
  --first            Move to first pending position
  --last             Move to end

JSON:

{"cmd": "step.move", "from": 5, "position": {"after": 2}}

!step export

!step export [--changes-only] [--with-comments]

JSON:

{"cmd": "step.export", "changesOnly": false, "withComments": true}

Validation Rules

  1. Index bounds: Must be 1 ≤ index ≤ total_steps
  2. Completed steps: Cannot edit, remove, or move completed steps
  3. Current step: Cannot remove or move the currently executing step (can edit for next run if step-back)
  4. Position conflicts: --at, --after, --before, --first, --last are mutually exclusive
  5. Type-specific options: --script, --shell, --working-directory only for run; --action, --with only for uses
  6. Required values: --at, --after, --before, --to require an index value

Error Codes

Code Description
INVALID_INDEX Index out of range or refers to completed/current step
INVALID_COMMAND Unknown command
INVALID_OPTION Unknown or conflicting options
INVALID_TYPE Invalid step type (not "run" or "uses")
ACTION_DOWNLOAD_FAILED Failed to download action for uses step
PARSE_ERROR Failed to parse command/JSON

Future Commands (Reserved)

These command names are reserved for future implementation:

!step duplicate <index>      # Clone a step
!step enable <index>         # Re-enable a disabled step
!step disable <index>        # Skip step without removing
!step inspect <index>        # Show detailed step info
!step reset <index>          # Revert modifications
!step import                 # Add steps from YAML

Notes for Production

  1. Action Restrictions: Before production, must integrate with organization/enterprise action policies (allowed lists, verified creators, etc.)

  2. Security: Dynamic step execution has security implications - ensure proper sandboxing and audit logging

  3. Persistence: Consider whether modifications should persist across step-back operations

  4. Undo: Consider adding !step undo for reverting last operation