Skip to content

Commit 9a9d82a

Browse files
Merge pull request #3 from dstackai/feat/program-variables-support
Add program variable support with preview diffs and docs
2 parents 3674256 + e3f4c87 commit 9a9d82a

8 files changed

Lines changed: 1047 additions & 35 deletions

File tree

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,22 @@ CLAUDEFORM_VERSION=v0.0.2 curl -fsSL https://raw.githubusercontent.com/dstackai/
8282
cf apply -f examples/smoke.md
8383
```
8484

85+
Override a program variable for one run:
86+
87+
```bash
88+
cf apply -f examples/smoke.md --var SMOKE_VALUE=YU
89+
```
90+
91+
The confirmation preview includes a variables summary, for example: `variables: 1 value changed, 0 added, 0 removed`.
92+
93+
Program variables are defined in frontmatter under `variables` (`NAME: {}` for required, `NAME: { default: "..." }` for optional defaults) and referenced as `${{ var.NAME }}`.
94+
8595
Example output (will vary by session/model):
8696

8797
```text
8898
cf apply -f examples/smoke.md
8999
Last session: 019d5843-eb2d-70b1-b49a-343033117944 (success, 43m ago)
90-
program diff: examples/smoke.md unchanged
100+
program: examples/smoke.md unchanged
91101
changes: 0 files
92102
Proceed? [y/N] y
93103
session 019d586b-aa65-78b2-8a0d-27b5543c59bb

contrib/ARCHITECTURE.md

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Claudeform Architecture
22

3-
Last updated: 2026-04-04
3+
Last updated: 2026-04-06
44
Status: v0 (implemented baseline)
55

66
## 1) Product Goal
@@ -9,7 +9,7 @@ Claudeform runs agent work from markdown files instead of chat windows.
99

1010
A **program** is one markdown file (`*.md`) representing one task.
1111

12-
- frontmatter is tool-owned and strict (`id`, `model`)
12+
- frontmatter is tool-owned and strict (`id`, `model`, `variables`)
1313
- markdown body is agent-facing and free-form
1414

1515
## 2) Implemented v0 Scope
@@ -57,6 +57,11 @@ Frontmatter (strict):
5757

5858
- `id` (optional)
5959
- `model` (optional override)
60+
- `variables` (optional map)
61+
- key: variable name (`[A-Za-z_][A-Za-z0-9_]*`)
62+
- value:
63+
- required (no default): `NAME: {}`
64+
- optional default: `NAME: { default: "value" }`
6065

6166
Program key resolution:
6267

@@ -65,19 +70,30 @@ Program key resolution:
6570

6671
Markdown body remains untyped in v0 and is interpreted by the agent.
6772

73+
Variable rules:
74+
75+
1. markdown may reference variables as `${{ var.NAME }}`
76+
2. referenced variables must be defined in frontmatter
77+
3. apply-time `--var NAME=VALUE` overrides frontmatter default
78+
4. variables without default are required at apply time
79+
6880
## 4) Apply Session Flow (Current Behavior)
6981

7082
1. Load program + config and resolve model.
71-
2. Build preview from last session context:
83+
2. Resolve program variables (frontmatter defaults + CLI `--var` overrides).
84+
3. Validate variable definitions and `${{ var.NAME }}` references.
85+
4. Build preview from last session context:
7286
- last session status/summary (if exists)
7387
- program diff vs last session snapshot (if available)
74-
3. Ask for confirmation (interactive default; skipped by `--yes`).
75-
4. Run provider in the current workspace (no temp workspace copy).
76-
5. Stream provider events to terminal and persist artifacts.
77-
6. Read agent status from `.claudeform/agent_result.json` (required).
78-
7. Collect reported changed files (events-first, manifest fallback).
79-
8. Persist session artifacts + history record.
80-
9. Persist program snapshot (`program.md`) on success.
88+
- variable diff vs last session variable snapshot (if available)
89+
5. Ask for confirmation (interactive default; skipped by `--yes`).
90+
6. Write runtime variables file (`.claudeform/agent_variables.json`) when variables are present.
91+
7. Run provider in the current workspace (no temp workspace copy).
92+
8. Stream provider events to terminal and persist artifacts.
93+
9. Read agent status from `.claudeform/agent_result.json` (required).
94+
10. Collect reported changed files (events-first, manifest fallback).
95+
11. Persist session artifacts + history record.
96+
12. Persist program snapshot (`program.md`) and variable snapshot (`variables.json`) on success.
8197

8298
## 5) State and Storage Layout
8399

@@ -93,6 +109,7 @@ Per program/session:
93109
- `<cwd>/.claudeform/programs/<program_id>/sessions/<session_id>/outcome.json`
94110
- `<cwd>/.claudeform/programs/<program_id>/sessions/<session_id>/output.md` (Claudeform summary)
95111
- `<cwd>/.claudeform/programs/<program_id>/sessions/<session_id>/program.md` (success snapshot)
112+
- `<cwd>/.claudeform/programs/<program_id>/sessions/<session_id>/variables.json` (success snapshot)
96113
- `<cwd>/.claudeform/programs/<program_id>/sessions/<session_id>/commands/*` (captured command outputs)
97114
- `<cwd>/.claudeform/programs/<program_id>/sessions/<session_id>/messages/*` (captured message outputs)
98115

@@ -109,6 +126,7 @@ Agent may write:
109126
- `<cwd>/.claudeform/agent_result.json` (required)
110127
- `<cwd>/.claudeform/agent_output.md` (optional human summary)
111128
- `<cwd>/.claudeform/agent_outputs.json` (optional fallback list of changed files)
129+
- `<cwd>/.claudeform/agent_variables.json` (runtime resolved variable values provided by Claudeform)
112130

113131
These files are execution protocol files, not user deliverables.
114132

@@ -134,23 +152,21 @@ Actual:
134152

135153
These items are intentionally deferred. Each item describes desired product capability, not implementation.
136154

137-
1. Program variables support
138-
Goal: allow programs to define reusable runtime inputs with clear behavior.
139-
2. Memory support
155+
1. Memory support
140156
Goal: support durable context across sessions with predictable usage rules.
141-
3. Plan support
157+
2. Plan support
142158
Goal: support planning as a first-class workflow, separate from execution.
143-
4. Interrupted/canceled session handling
159+
3. Interrupted/canceled session handling
144160
Goal: represent and communicate non-completed runs clearly to users.
145-
5. Changes/diff reliability and consistency
161+
4. Changes/diff reliability and consistency
146162
Goal: for the same session, preview, apply output, debug output, and history should report the same changed-file set and line counts, with generated/noise files handled consistently.
147-
6. Agent-reported changes as single source of truth
163+
5. Agent-reported changes as single source of truth
148164
Goal: remove legacy local diff-based change reporting and use agent-reported change data consistently across apply, debug, and history.
149-
7. MCP and broader tool integration model
165+
6. MCP and broader tool integration model
150166
Goal: support richer external tool and integration patterns.
151-
8. Multi-agent orchestration model
167+
7. Multi-agent orchestration model
152168
Goal: support coordinated workflows that involve more than one agent.
153-
9. Additional providers beyond Codex
169+
8. Additional providers beyond Codex
154170
Goal: support multiple model providers in a consistent user experience.
155-
10. Improved session storage and retrieval performance
171+
9. Improved session storage and retrieval performance
156172
Goal: keep history/state operations fast and scalable as usage grows.

contrib/DEVELOPMENT.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,27 @@ Skip confirmation prompt:
155155
cargo run -p claudeform -- apply -f examples/smoke.md --yes
156156
```
157157

158+
Pass program variables at apply time (repeat `--var` as needed):
159+
160+
```bash
161+
# uses smoke frontmatter default (SMOKE_OK)
162+
cargo run -p claudeform -- apply -f examples/smoke.md --yes
163+
164+
# smoke has default SMOKE_OK, and this overrides it for one run
165+
cargo run -p claudeform -- apply -f examples/smoke.md --var SMOKE_VALUE=YU --yes
166+
```
167+
168+
Notes:
169+
170+
- Variables are defined in program frontmatter under `variables`.
171+
- Required variable syntax (no default): `NAME: {}`.
172+
- Optional variable syntax with default: `NAME: { default: "value" }`.
173+
- Program body references variables via `${{ var.NAME }}`.
174+
- Confirmation preview includes a variable-diff summary against last session when available.
175+
- Runtime resolved values are written to `.claudeform/agent_variables.json` for the agent.
176+
- Successful sessions persist a snapshot at `.claudeform/programs/<program_id>/sessions/<session_id>/variables.json`.
177+
- If a required variable is missing at apply time, apply fails before provider execution.
178+
158179
Reset session history:
159180

160181
```bash

crates/claudeform-cli/src/lib.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ enum Commands {
5454
#[arg(short = 'f', long = "file")]
5555
file: PathBuf,
5656

57+
/// Program variable value (`NAME=VALUE`). Repeatable.
58+
#[arg(long = "var", value_name = "NAME=VALUE", action = ArgAction::Append)]
59+
vars: Vec<String>,
60+
5761
/// Auto-approve apply without interactive confirmation prompt.
5862
#[arg(short = 'y', long)]
5963
yes: bool,
@@ -139,6 +143,7 @@ fn real_main() -> Result<()> {
139143
match cli.command {
140144
Commands::Apply {
141145
file,
146+
vars,
142147
yes,
143148
debug,
144149
progress_mode,
@@ -167,6 +172,7 @@ fn real_main() -> Result<()> {
167172
};
168173
let confirm = interactive_shell && !yes;
169174
let sandbox_mode: SandboxMode = sandbox_mode.into();
175+
let program_variables = parse_apply_variables(&vars)?;
170176

171177
if debug {
172178
let caps = runner.capabilities();
@@ -196,6 +202,7 @@ fn real_main() -> Result<()> {
196202
&ApplyRequest {
197203
workspace_root,
198204
program_path: file,
205+
program_variables,
199206
confirm,
200207
debug,
201208
progress: true,
@@ -331,6 +338,44 @@ fn confirm_history_reset_interactive(target: &str) -> Result<bool> {
331338
Ok(matches!(line.trim(), "y" | "Y" | "yes" | "YES"))
332339
}
333340

341+
fn parse_apply_variables(entries: &[String]) -> Result<BTreeMap<String, String>> {
342+
let mut out = BTreeMap::new();
343+
for raw in entries {
344+
let Some((name_raw, value_raw)) = raw.split_once('=') else {
345+
return Err(anyhow!("invalid --var '{}': expected NAME=VALUE", raw));
346+
};
347+
let name = name_raw.trim();
348+
if name.is_empty() {
349+
return Err(anyhow!(
350+
"invalid --var '{}': variable name cannot be empty",
351+
raw
352+
));
353+
}
354+
if !is_valid_variable_name(name) {
355+
return Err(anyhow!(
356+
"invalid --var '{}': NAME must match [A-Za-z_][A-Za-z0-9_]*",
357+
raw
358+
));
359+
}
360+
if out.contains_key(name) {
361+
return Err(anyhow!("duplicate --var for '{}'", name));
362+
}
363+
out.insert(name.to_string(), value_raw.to_string());
364+
}
365+
Ok(out)
366+
}
367+
368+
fn is_valid_variable_name(name: &str) -> bool {
369+
let mut chars = name.chars();
370+
let Some(first) = chars.next() else {
371+
return false;
372+
};
373+
if !(first.is_ascii_alphabetic() || first == '_') {
374+
return false;
375+
}
376+
chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
377+
}
378+
334379
fn yes_no(v: bool) -> &'static str {
335380
if v {
336381
"yes"
@@ -737,4 +782,19 @@ mod tests {
737782
]
738783
);
739784
}
785+
786+
#[test]
787+
fn parse_apply_variables_parses_name_value_pairs() {
788+
let vars =
789+
parse_apply_variables(&["APP_NAME=calc".to_string(), "APP_PORT=8080".to_string()])
790+
.expect("must parse");
791+
assert_eq!(vars.get("APP_NAME").map(String::as_str), Some("calc"));
792+
assert_eq!(vars.get("APP_PORT").map(String::as_str), Some("8080"));
793+
}
794+
795+
#[test]
796+
fn parse_apply_variables_rejects_invalid_name() {
797+
let err = parse_apply_variables(&["BAD-NAME=x".to_string()]).expect_err("must fail");
798+
assert!(format!("{:#}", err).contains("NAME must match"));
799+
}
740800
}

0 commit comments

Comments
 (0)