diff --git a/.opencode/plans/dap-step-manipulation.md b/.opencode/plans/dap-step-manipulation.md new file mode 100644 index 000000000..1d6f973cf --- /dev/null +++ b/.opencode/plans/dap-step-manipulation.md @@ -0,0 +1,1119 @@ +# 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 [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 ` — 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); // "!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: + ```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 `!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:** + ```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 (!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:** + ```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 (!step 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: 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:** + ```csharp + 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:** + ```csharp + 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() ?? false }, + "step.add" => ParseJsonAddCommand(obj), + "step.edit" => ParseJsonEditCommand(obj), + "step.remove" => new RemoveCommand { Index = obj["index"].Value() }, + "step.move" => ParseJsonMoveCommand(obj), + "step.export" => new ExportCommand { + ChangesOnly = obj["changesOnly"]?.Value() ?? false, + WithComments = obj["withComments"]?.Value() ?? false + }, + _ => throw new StepCommandException($"Unknown command: {cmd}") + }; + } + ``` + +3. **JSON response format:** + ```csharp + // 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:** + ```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 JSON API:** + ```javascript + 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:** +```json +{"cmd": "step.list", "verbose": false} +``` + +### !step add + +**Run step:** +``` +!step add run "