Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Executant is a TypeScript CLI tool (`src/`) that executes YAML-defined workflows
- `self_healing: true` - Optional (defaults to `false`; opt-in per step), automatically passes script failures to Claude for fixing
- `llm_as_judge: true` - Optional, evaluates step quality and retries up to 5 times if needed
- `allowed_tools` - Optional list restricting which tools are available for a prompt step. Applies to both Claude and OpenCode providers. Omit entirely for no restrictions (default — all tools available). `[]` = text-only mode (no tools). `[bash, read]` = only those tools. Tool names are case-insensitive (`Bash` and `bash` both work).
- `append_system_prompt` - Optional, prompt steps only. Appended to the CLI's own system prompt for this step via `--append-system-prompt`, rather than living inside the user-turn `prompt:` text. Use it for a rule the model must not trade off against everything else in a long prompt (e.g. "never narrate outside the deliverable") — system-level instructions get materially stronger adherence than the same words placed in the user turn. Supports `{{var}}` substitution like `prompt:` does.
- `context` - Optional list of var names whose values are file paths; file contents are prepended to the prompt at runtime
- `forEach` - Optional inline array or shell command (newline-split stdout); runs the inner step once per item with `{{item}}` substituted
- `repeat: N` - Runs the step N times sequentially (compiles to a ForEachTask at load time); mutually exclusive with `forEach`; `{{item}}` is the 1-based iteration number
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ Step-level `provider`, `model`, and `agent` fields take priority over env vars.
- Omit entirely → all tools available (default)
- `allowed_tools: []` → text-only mode, no tools
- `allowed_tools: [Bash, Read, Write]` → only those tools; names are case-insensitive
- **`append_system_prompt`** — prompt steps only; appended to the CLI's own system prompt
via `--append-system-prompt` instead of living inside `prompt:`'s user-turn text. A rule
placed here holds up far better than the same words buried in a long prompt — reach for
it for anything the model must never trade off against the rest of the instructions
(e.g. "never narrate outside the deliverable"). Supports `{{var}}` substitution.

```yaml
steps:
Expand All @@ -306,6 +311,14 @@ steps:
- name: summarise
prompt: Write a one-paragraph summary.
allowed_tools: [] # no tools — pure text generation

- name: pr_summary
prompt: |
Write the pull-request description... deliver it wrapped in <PR_BODY> markers.
append_system_prompt: >-
You are running non-interactively and unattended. Never emit narration,
commentary, or a preamble in your text output — reason and decide silently
via tool calls; the only text you output is the exact deliverable requested.
```

```yaml
Expand Down
9 changes: 9 additions & 0 deletions src/load-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const RawStepSchema: z.ZodType<RawStep> = z.lazy(() =>
timeout_seconds: z.number().positive().optional(),
provider: z.enum(["claude", "opencode"]).optional(),
model: z.string().optional(),
append_system_prompt: z.string().optional(),
agent: z.string().optional(),
workflow: z.string().optional(),
vars: z.record(z.string(), z.string()).optional(),
Expand Down Expand Up @@ -316,6 +317,14 @@ function convertInnerStep(
model: step.model ?? DEFAULT_MODEL,
...(step.provider && { provider: step.provider }),
...(step.agent && { agent: step.agent }),
...(step.append_system_prompt && {
appendSystemPrompt: substituteVars(
step.append_system_prompt,
vars,
name,
"append_system_prompt",
),
}),
...(contextFiles.length > 0 && { contextFiles }),
...(step.output && {
output: resolveOutputFile(step.output, vars, name),
Expand Down
43 changes: 43 additions & 0 deletions src/tests/load-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,49 @@ steps:
assert.equal(task.model, "llama-qwen7b/qwen2.5-coder-7b");
});

test("append_system_prompt is passed through to ClaudeTask", () => {
const file = tmpYaml(`
goal: test
steps:
- name: implement
append_system_prompt: Never narrate. Reply with only the deliverable.
prompt: Do the work
`);
const wf = loadWorkflow(file);
const task = wf.tasks[0] as ClaudeTask;
assert.equal(
task.appendSystemPrompt,
"Never narrate. Reply with only the deliverable.",
);
});

test("append_system_prompt is omitted from ClaudeTask when not set", () => {
const file = tmpYaml(`
goal: test
steps:
- name: implement
prompt: Do the work
`);
const wf = loadWorkflow(file);
const task = wf.tasks[0] as ClaudeTask;
assert.equal(task.appendSystemPrompt, undefined);
});

test("append_system_prompt substitutes vars like prompt does", () => {
const file = tmpYaml(`
goal: test
vars:
role: reviewer
steps:
- name: implement
append_system_prompt: You are acting as a {{role}}.
prompt: Do the work
`);
const wf = loadWorkflow(file);
const task = wf.tasks[0] as ClaudeTask;
assert.equal(task.appendSystemPrompt, "You are acting as a reviewer.");
});

test("agent field is passed through to ClaudeTask", () => {
const file = tmpYaml(`
goal: test
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,8 @@ export type RawStep = {
provider?: AgentProvider;
/** Model override for this step. */
model?: string;
/** Appended to the CLI's own system prompt for this step only. */
append_system_prompt?: string;
/** OpenCode agent name. */
agent?: string;
/** Local path or URL to another workflow, run as a nested sub-run. */
Expand Down
Loading