# 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 - [x] **Chunk 1:** Command Parser & Infrastructure - [x] **Chunk 2:** Step Serializer (ActionStep → YAML) - [x] **Chunk 3:** Step Factory (Create new steps) - [x] **Chunk 4:** Step Manipulator (Queue operations) - [x] **Chunk 5:** REPL Commands (steps list, steps add run, steps edit, steps remove, steps move) - [x] **Chunk 6:** Action Download Integration (steps add uses) - [x] **Chunk 7:** Export Command (steps export) - [x] **Chunk 8:** Output Format Flag (--output text|json for programmatic use) - [x] **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 `--output` flag for text/JSON response format - **Non-goal:** Full workflow file reconstruction (steps section only) - **Non-goal:** Production action restriction enforcement (noted for later) ## Command API Specification ### Grammar ``` steps [target] [options] [--output text|json] ``` ### Output Format All commands support the `--output` flag to control response format: - `--output text` (default) - Human-readable text output - `--output json` - JSON output for programmatic use - Short form: `-o json`, `-o text` - Equals form: `--output=json`, `--output=text` ### 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 | |---------|---------|---------| | `steps list` | Show all steps | `steps list --verbose` | | `steps add` | Add new step | `steps add run "npm test" --after 3` | | `steps edit` | Modify step | `steps edit 4 --script "npm run test:ci"` | | `steps remove` | Delete step | `steps remove 5` | | `steps move` | Reorder step | `steps move 5 --after 2` | | `steps export` | Generate YAML | `steps export --with-comments` | ### Position Modifiers For `steps add` and `steps move`: - `--at ` — Insert at specific position - `--after ` — Insert after step - `--before ` — 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: ```csharp public interface IStepCommandParser { StepCommand Parse(string input); // "steps add run \"echo hello\" --after 3" bool IsStepCommand(string input); // Starts with "steps " or equals "steps" } public enum OutputFormat { Text, Json } public abstract class StepCommand { public OutputFormat Output { get; set; } = OutputFormat.Text; } 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: ```csharp 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: ```csharp 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():** ```csharp // 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:** ```csharp public interface IStepSerializer { string ToYaml(Pipelines.ActionStep step); string ToYaml(IEnumerable steps, bool withComments = false); } ``` 2. **Handle both step types:** For `run` steps (ScriptReference): ```yaml - 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): ```yaml - 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:** ```csharp public interface IStepFactory : IRunnerService { Pipelines.ActionStep CreateRunStep( string script, string name = null, string shell = null, string workingDirectory = null, Dictionary env = null, string condition = null, bool continueOnError = false, int? timeoutMinutes = null); Pipelines.ActionStep CreateUsesStep( string actionReference, // "owner/repo@ref" string name = null, Dictionary with = null, Dictionary 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:** ```csharp 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:** ```csharp 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): ```csharp public IActionRunner WrapInRunner(Pipelines.ActionStep step, IExecutionContext jobContext, ActionRunStage stage) { var runner = HostContext.CreateService(); 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: ```csharp 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: ```csharp 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:** ```csharp public interface IStepManipulator : IRunnerService { // Initialize with job context void Initialize(IExecutionContext jobContext, int currentStepIndex); void UpdateCurrentIndex(int index); // Query IReadOnlyList 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 edit); // Change tracking IReadOnlyList 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:** ```csharp 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 `steps list`, `steps add run`, `steps edit`, `steps remove`, `steps 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:** ```csharp public interface IStepCommandHandler : IRunnerService { Task HandleAsync(StepCommand command, IExecutionContext context); } ``` 2. **Command implementations:** **List:** ```csharp case ListCommand list: var steps = _manipulator.GetAllSteps(); var output = FormatStepList(steps, list.Verbose); return new StepCommandResult { Success = true, Result = steps, Message = output }; ``` **Add run:** ```csharp 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:** ```csharp 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:** ```csharp case RemoveCommand remove: ValidatePendingIndex(remove.Index); _manipulator.RemoveStep(remove.Index); return new StepCommandResult { Success = true, Message = $"Step {remove.Index} removed" }; ``` **Move:** ```csharp 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:** ```csharp private async Task 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 (steps add uses) **Goal:** Support `steps add uses` with full action download. **Files to modify:** - `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs` **Details:** 1. **Add uses command handling:** ```csharp 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(); 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):** ```csharp // 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 (steps export) **Goal:** Generate YAML output for modified steps. **Files to modify:** - `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs` **Details:** 1. **Export command handling:** ```csharp case ExportCommand export: var steps = _manipulator.GetAllSteps(); var changes = _manipulator.GetChanges(); IEnumerable 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:** ```yaml 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: Output Format Flag for Browser Extension **Goal:** Add `--output` flag support for programmatic access (replaces separate JSON API). **Files to modify:** - `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs` - `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs` **Details:** 1. **Add OutputFormat enum and property to base StepCommand:** ```csharp public enum OutputFormat { Text, Json } public abstract class StepCommand { public OutputFormat Output { get; set; } = OutputFormat.Text; } ``` 2. **Parse `--output` flag in command parser:** ```csharp // Recognize --output json, --output text, -o json, -o text, --output=json private OutputFormat ParseOutputFlag(List tokens) { for (int i = 0; i < tokens.Count; i++) { var token = tokens[i].ToLower(); if (token == "--output" || token == "-o") { if (i + 1 < tokens.Count) { var format = tokens[i + 1].ToLower(); tokens.RemoveAt(i); tokens.RemoveAt(i); return format == "json" ? OutputFormat.Json : OutputFormat.Text; } } else if (token.StartsWith("--output=")) { var format = token.Substring("--output=".Length); tokens.RemoveAt(i); return format == "json" ? OutputFormat.Json : OutputFormat.Text; } } return OutputFormat.Text; } ``` 3. **Format responses based on Output property in handler:** ```csharp if (command.Output == OutputFormat.Json) { return new StepCommandResult { Success = true, Message = JsonConvert.SerializeObject(new { Success = true, Result = data }) }; } else { return new StepCommandResult { Success = true, Message = FormatAsText(data) }; } ``` 4. **Browser extension sends REPL commands with `--output json`:** ```javascript // Browser extension builds command strings like: // "steps list --output json" // "steps add run \"echo test\" --after 3 --output json" ``` **Benefits over separate JSON API:** - Single code path for parsing all commands - Easier debugging (UI sends same commands humans would type) - Commands can be copy-pasted from UI to console for testing - Less code to maintain **Testing:** - All commands with `--output json` return valid JSON - All commands with `--output text` (or default) return human-readable text - Short form `-o json` works correctly **Estimated effort:** Small --- ### 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:** ```html
Steps
``` 2. **Step List Rendering:** ```javascript function renderSteps(steps) { return steps.map(step => `
${getStatusIcon(step.status)} ${step.index}. ${step.name} ${step.change ? `[${step.change}]` : ''} ${step.type} ${step.status === 'pending' ? renderStepActions(step) : ''}
`).join(''); } ``` 3. **Add Step Dialog:** ```javascript 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:** ```javascript function showStepContextMenu(stepIndex, event) { // Edit, Move Up, Move Down, Delete } ``` 5. **Export Modal:** ```javascript function showExportModal(yaml) { // Code view with syntax highlighting // Copy to clipboard button } ``` 6. **Send commands via REPL format with `--output json`:** ```javascript async function addStep(type, options) { const cmd = buildStepCommand('step.add', { type, ...options }); const response = await sendEvaluate(cmd); // e.g., "steps add run \"echo test\" --output json" refreshStepList(); } async function loadSteps() { const response = await sendEvaluate('steps list --output json'); // Parse JSON response } ``` **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 | | `StepCommandParser.cs` | 8 | Add `--output` flag parsing | | `StepCommandHandler.cs` | 8 | Format responses based on OutputFormat | | `content.js` | 9 | Steps panel, dialogs, export modal, build REPL commands | | `content.css` | 9 | Styling for new UI elements | | `background.js` | 9 | Helper functions if needed | --- ## Command API Full Reference ### Output Format Flag All commands support the `--output` flag: ``` steps ... --output text # Human-readable output (default) steps ... --output json # JSON output for programmatic use steps ... -o json # Short form steps ... --output=json # Equals form ``` The browser extension uses `--output json` for all commands to receive structured responses. ### steps list ``` steps list [--verbose] [--output text|json] ``` 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 output (`steps list --output json`):** ```json { "Success": true, "Result": [ {"index": 1, "name": "Checkout", "type": "uses", "typeDetail": "actions/checkout@v4", "status": "completed"}, {"index": 2, "name": "Setup Node", "type": "uses", "typeDetail": "actions/setup-node@v4", "status": "completed"}, {"index": 3, "name": "Install deps", "type": "run", "typeDetail": "npm ci", "status": "current"}, {"index": 4, "name": "Run tests", "type": "run", "typeDetail": "npm test", "status": "pending", "change": "MODIFIED"}, {"index": 5, "name": "Build", "type": "run", "typeDetail": "npm run build", "status": "pending", "change": "ADDED"} ] } ``` ### steps add **Run step:** ``` steps add run "