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

1120 lines
32 KiB
Markdown

# 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 (!step list, !step add run, !step edit, !step remove, !step move)
- [x] **Chunk 6:** Action Download Integration (!step add uses)
- [x] **Chunk 7:** Export Command (!step export)
- [x] **Chunk 8:** JSON API for Browser Extension
- [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 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:
```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<StepInfo> 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<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:**
```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<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:
```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<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:**
```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<StepCommandResult> 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<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:**
```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<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):**
```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<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:**
```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<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:**
```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
<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:**
```javascript
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:**
```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 "<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):**
```json
{
"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):**
```json
{
"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:**
```json
{"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:**
```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:**
```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:**
```json
{"cmd": "step.move", "from": 5, "position": {"after": 2}}
```
### !step export
```
!step export [--changes-only] [--with-comments]
```
**JSON:**
```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