1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# Profile-carried agent args implementation plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let a ramekin profile pin CLI flags for the agent binary, so a `pi-bedrock` profile can select `--provider amazon-bedrock` on its own.
**Architecture:** Add an `args: Vec<String>` field to `Profile`, parsed from one or more `args` nodes in a profile's KDL block. Thread the active profile's args into the generated compose command, placed after the prompt flag and before the per-run CLI trailing args so the latter still override.
**Tech Stack:** Rust (edition 2024), `kdl` crate for parsing, `serde_yaml` for compose generation, `miette` for errors.
## Global Constraints
- Rust edition 2024. Error handling uses `miette` (`bail!`, `miette!`, `.into_diagnostic()`); no `.unwrap()` in non-test code.
- File I/O uses `fs-err`, never `std::fs` (clippy enforces this).
- All CI checks must pass: `cargo fmt --all --check`, `cargo clippy --workspace -- -D warnings`, `cargo test --workspace`. Run `just` before committing.
- VCS is jj, not git. Each logical change is its own commit (`jj commit -m '...'`). Commit subjects are plain English, capitalized, under 60 chars, no conventional-commit prefixes. AI-drafted messages carry an `Assisted-by: Claude Opus 4.8 via pi` trailer.
- Unknown KDL nodes and fields fail loudly (`bail!`).
---
### Task 1: Add the `args` field to Profile and its KDL parsing
**Files:**
- Modify: `src/config.rs` — `Profile` struct, `Profile::builtin`, `parse_profile`, new `parse_args` helper, the `scoped` test helper
- Test: `src/config.rs` (inline `#[cfg(test)] mod tests`)
**Interfaces:**
- Produces: `Profile.args: Vec<String>` (public field on the existing `pub struct Profile`); `fn parse_args(node: &KdlNode) -> Result<Vec<String>>` (private).
- [ ] **Step 1: Write the failing tests**
Add these three tests inside the `tests` module in `src/config.rs` (near the existing `parse_profile_definition` test):
```rust
#[test]
fn parse_profile_with_args() {
let raw = parse_config(
r#"
profile "pi-bedrock" {
agent "pi"
args "--provider" "amazon-bedrock"
}
"#,
)
.unwrap();
let p = &raw.profiles[0];
assert_eq!(p.args, vec!["--provider", "amazon-bedrock"]);
}
#[test]
fn parse_profile_concatenates_multiple_args_nodes() {
let raw = parse_config(
r#"
profile "x" {
agent "pi"
args "--provider" "amazon-bedrock"
args "--model" "some-model"
}
"#,
)
.unwrap();
assert_eq!(
raw.profiles[0].args,
vec!["--provider", "amazon-bedrock", "--model", "some-model"]
);
}
#[test]
fn parse_profile_rejects_non_string_args() {
let result = parse_config("profile \"x\" {\n agent \"pi\"\n args 42\n}");
assert!(result.is_err());
}
```
- [ ] **Step 2: Run the tests to verify they fail**
Run: `cargo test --lib parse_profile_with_args parse_profile_concatenates_multiple_args_nodes parse_profile_rejects_non_string_args`
Expected: compile error — `Profile` has no field `args` (and `parse_args` undefined).
- [ ] **Step 3: Add the `args` field to the struct and both literal constructors**
In `src/config.rs`, add the field to `Profile` (after `mounts`):
```rust
pub struct Profile {
pub name: String,
pub agent: Agent,
pub env: Vec<EnvVar>,
pub mounts: Vec<Mount>,
pub args: Vec<String>,
}
```
In `Profile::builtin`, add `args: Vec::new()` to the constructed `Self { ... }`:
```rust
.map(|agent| Self {
name: agent.name().to_string(),
agent,
env: Vec::new(),
mounts: Vec::new(),
args: Vec::new(),
})
```
In the `scoped` test helper's `Profile { ... }` literal, add `args: Vec::new()` after `mounts: Vec::new()`.
- [ ] **Step 4: Add the `parse_args` helper**
In `src/config.rs`, add next to `parse_mounts`/`parse_env`:
```rust
/// Parse an `args` node: bare string entries passed verbatim to the agent
/// binary (`args "--provider" "amazon-bedrock"`). No properties, no block.
fn parse_args(node: &KdlNode) -> Result<Vec<String>> {
if node.children().is_some() {
bail!("`args` takes inline string values (args \"--flag\" \"value\"), not a block");
}
node.entries()
.iter()
.map(|entry| {
if entry.name().is_some() {
bail!("`args` takes bare string values, not properties");
}
entry
.value()
.as_string()
.map(str::to_string)
.ok_or_else(|| miette!("`args` takes string values, got {}", entry.value()))
})
.collect()
}
```
- [ ] **Step 5: Wire `args` into `parse_profile`**
In `parse_profile`, add the accumulator and match arm, then include it in the returned `Profile`:
```rust
let mut agent = None;
let mut env = Vec::new();
let mut mounts = Vec::new();
let mut args = Vec::new();
for child in children.nodes() {
match child.name().value() {
"agent" => agent = Some(Agent::parse(&single_string_arg(child)?)?),
"env" => env.extend(parse_env(child)?),
"mounts" => mounts.extend(parse_mounts(child)?),
"args" => args.extend(parse_args(child)?),
other => bail!("unknown `profile` field `{other}`"),
}
}
Ok(ProfileNode::Definition(Profile {
agent: agent
.ok_or_else(|| miette!("profile `{name}` is missing `agent` (`pi` or `claude`)"))?,
name,
env,
mounts,
args,
}))
```
- [ ] **Step 6: Run the tests to verify they pass**
Run: `cargo test --lib`
Expected: PASS, including the three new tests and all pre-existing config tests.
- [ ] **Step 7: Commit**
```bash
just
jj commit -m 'config: let profiles carry agent CLI args
Add an `args` field to Profile, parsed from one or more `args` nodes in a
profile block, so a profile can pin flags the agent needs (pi selects
Bedrock via --provider, which has no env equivalent). Entries concatenate
like env/mounts blocks; args ride with the wholesale profile-merge rule.
Assisted-by: Claude Opus 4.8 via pi'
```
---
### Task 2: Thread profile args into the container command
**Files:**
- Modify: `src/main.rs` — `ComposeParams` struct, its destructure in `generate_compose`, the command assembly, the `run` call site, the `compose_params` test helper
- Test: `src/main.rs` (inline `#[cfg(test)] mod tests`)
**Interfaces:**
- Consumes: `Profile.args` from Task 1 (via `self.config.profile.args`).
- Produces: `ComposeParams.profile_args: &'a [String]`. Command layout becomes `[prompt_flag, PROMPT_TARGET] ++ profile_args ++ agent_args`.
- [ ] **Step 1: Write the failing test**
Add to the `tests` module in `src/main.rs` (near `generate_compose_long_form_binds`):
```rust
#[test]
fn generate_compose_places_profile_args_before_cli_args() {
let profile_args = vec!["--provider".to_string(), "amazon-bedrock".to_string()];
let cli_args = vec!["--model".to_string(), "override".to_string()];
let mut params = compose_params(&[], &[]);
params.profile_args = &profile_args;
params.agent_args = &cli_args;
let yaml = generate_compose(params);
let prompt = yaml.find(PROMPT_TARGET).expect("prompt target missing");
let provider = yaml.find("amazon-bedrock").expect("profile arg missing");
let model = yaml.find("override").expect("cli arg missing");
assert!(
prompt < provider && provider < model,
"expected prompt < profile arg < cli arg, got {yaml}"
);
}
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `cargo test --lib generate_compose_places_profile_args_before_cli_args`
Expected: compile error — `ComposeParams` has no field `profile_args`.
- [ ] **Step 3: Add the `profile_args` field to `ComposeParams`**
In `src/main.rs`, add it just before `agent_args` in the struct:
```rust
prompt_flag: &'a str,
/// CLI flags the active profile pins for the agent binary. Placed before
/// the per-run trailing args so a `ramekin -- ...` flag still wins.
profile_args: &'a [String],
agent_args: &'a [String],
```
- [ ] **Step 4: Destructure and use it in `generate_compose`**
Add `profile_args,` to the `let ComposeParams { ... } = params;` destructure (before `agent_args,`). Then update the command assembly:
```rust
// Always pass the prompt flag for the ramekin container context.
// Profile args come next; user-supplied CLI args come last so they win.
let command: Vec<String> = [prompt_flag.to_string(), PROMPT_TARGET.to_string()]
.into_iter()
.chain(profile_args.iter().cloned())
.chain(agent_args.iter().cloned())
.collect();
```
- [ ] **Step 5: Pass the active profile's args at the `run` call site**
In `run`, in the `generate_compose(ComposeParams { ... })` call, add `profile_args` just before `agent_args`:
```rust
prompt_flag: match agent {
config::Agent::Pi => "--append-system-prompt",
config::Agent::Claude => "--append-system-prompt-file",
},
profile_args: &self.config.profile.args,
agent_args,
```
- [ ] **Step 6: Update the `compose_params` test helper**
In the `compose_params` helper's returned `ComposeParams { ... }`, add `profile_args: &[],` before `agent_args: &[]`.
- [ ] **Step 7: Run the tests to verify they pass**
Run: `cargo test --lib`
Expected: PASS, including the new test and all pre-existing main tests.
- [ ] **Step 8: Commit**
```bash
just
jj commit -m 'Run the active profile'"'"'s pinned args
generate_compose now inserts profile.args between the prompt flag and the
per-run CLI trailing args, so selecting a profile applies its flags while
`ramekin -- ...` can still override them.
Assisted-by: Claude Opus 4.8 via pi'
```
---
### Task 3: Document the `args` field
**Files:**
- Modify: `AGENTS.md` (repo root) — the KDL grammar sentence and the profile/merge description
- Modify: `docs/config-redesign.md` — the "A profile is a named bundle" sentence and example
**Interfaces:**
- Consumes: nothing (prose only).
- Produces: nothing.
- [ ] **Step 1: Update `AGENTS.md`**
In the architecture note describing the grammar, extend the profile clause to mention `args`. Find:
> and `profile` nodes (with children = definition, bare = selection). Unknown nodes fail loudly.
Replace with:
> and `profile` nodes (with children = definition carrying `agent`/`env`/`mounts`/`args`, bare = selection). Unknown nodes fail loudly.
In the merge-layers sentence, find `profile (the active profile's env/mounts)` and change to `profile (the active profile's env, mounts, and args)`.
- [ ] **Step 2: Update `docs/config-redesign.md`**
Find:
> A profile is a named bundle: agent, env vars, extra mounts.
Replace with:
> A profile is a named bundle: agent, env vars, extra mounts, and agent CLI args.
Then, after the `pi-glm` example block, add a sentence and example showing `args`:
````markdown
A profile also pins CLI flags for the agent binary through `args`, for
providers the agent selects by flag rather than by environment (pi reaches
Amazon Bedrock through `--provider`, which has no env equivalent):
```kdl
profile "pi-bedrock" {
agent "pi"
args "--provider" "amazon-bedrock"
env {
AWS_PROFILE
AWS_REGION
}
mounts { "~/.aws" }
}
```
Args ride with the profile definition, so the wholesale profile-merge rule
covers them too; the run's trailing `ramekin -- ...` args come after and
override.
````
- [ ] **Step 3: Verify the docs read correctly**
Run: `rg -n "args" AGENTS.md docs/config-redesign.md`
Expected: the new mentions appear; skim them for accuracy.
- [ ] **Step 4: Commit**
```bash
jj commit -m 'docs: document profile args
Assisted-by: Claude Opus 4.8 via pi'
```
---
### Task 4: Add the pi-bedrock profile to the user config
**Files:**
- Modify: `~/.config/ramekin/config.kdl` (outside the repo — the machine's user config; not a ramekin-repo commit)
**Interfaces:**
- Consumes: the `args` field from Task 1 and its runtime wiring from Task 2.
- Produces: a selectable `pi-bedrock` profile.
Note: this file is a plain file on this machine (not a dotfiles symlink), so there is no separate repo to commit it to. This task is a config edit, not a version-control change.
- [ ] **Step 1: Append the profile**
Add to `~/.config/ramekin/config.kdl` (leave the existing `profile "claude"` selection as the default):
```kdl
// Pi against Amazon Bedrock. Pi selects the provider by flag (no env
// equivalent); the model is left to pi at run time. AWS creds come from an
// SSO profile — fill in the placeholders and `aws sso login` on the host.
// ~/.aws mounts read-only, so an expired SSO token means re-logging in on
// the host rather than a silent in-container refresh.
profile "pi-bedrock" {
agent "pi"
args "--provider" "amazon-bedrock"
env {
AWS_PROFILE "REPLACE_ME"
AWS_REGION "REPLACE_ME"
}
mounts { "~/.aws" }
}
```
- [ ] **Step 2: Verify the config parses and the profile resolves**
Run: `ramekin -p pi-bedrock config`
Expected: no parse error; the resolved config shows profile `pi-bedrock`, agent `pi`, the `~/.aws` mount (read-only), and the `AWS_PROFILE`/`AWS_REGION` env vars. (It resolves even with `REPLACE_ME` values — those only matter at agent run time.)
---
## Self-Review
Spec coverage: the `args` field and parsing (Task 1), command assembly with profile args before CLI args (Task 2), builtin profiles' empty args (Task 1, `Profile::builtin`), the pi-bedrock user profile (Task 4), and docs (Task 3) all map to the spec. The spec's out-of-scope items (settings.json fragment, CLI-arg parsing changes, Claude Bedrock) are correctly untouched.
Placeholder scan: `REPLACE_ME` in Task 4 is an intentional, spec-mandated placeholder in the user's config, not a plan gap. All code steps show complete code.
Type consistency: `Profile.args: Vec<String>` (Task 1) is consumed as `&self.config.profile.args` into `ComposeParams.profile_args: &[String]` (Task 2) — types line up. `parse_args` returns `Result<Vec<String>>`, matching the `args.extend(...)` accumulator.