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
5 changes: 5 additions & 0 deletions .codegraph/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CodeGraph data files — local to each machine, not for committing.
# Ignore everything in .codegraph/ except this file itself, so transient
# files (the database, daemon.pid, sockets, logs) never show up in git.
*
!.gitignore
1 change: 1 addition & 0 deletions .codex/AGENTS.md
1 change: 1 addition & 0 deletions .codex/instructions.md
48 changes: 23 additions & 25 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,54 +36,52 @@ node_modules/
pnpm-debug.log
perf_test_data/
tmp
.opencode/package-lock.json
# START AI Agent Symlinks
.agent/commands
.agent/commands.bak
.agent/commands.bak.*
.agent/rules/
.agent/rules/instructions.md
.agent/rules/instructions.md.bak
.agent/skills
.agent/skills.bak
.agent/skills.bak.*
.agent/skills/
.agents/skills/*.bak
.claude/commands
.claude/commands.bak
.claude/commands.bak.*
.claude/commands/
.claude/skills
.claude/skills.bak
.claude/skills.bak.*
.claude/skills/
.codex/AGENTS.md
.codex/AGENTS.md.bak.*
.codex/commands
.codex/commands.bak
.codex/commands.bak.*
.codex/config.toml
.codex/instructions.md
.codex/instructions.md.bak
.codex/skills
.codex/skills.bak
.codex/skills.bak.*
.gemini/commands
.gemini/commands.bak
.gemini/commands.bak.*
.gemini/commands/
.gemini/settings.json
.gemini/skills
.gemini/skills.bak
.gemini/skills.bak.*
.gemini/skills/
.github/agents
.github/agents.bak
.github/agents.bak.*
.github/copilot-instructions.md
.github/copilot-instructions.md.bak
.github/copilot-instructions.md.bak.*
.mcp.json
.opencode/command
.opencode/command.bak
.opencode/command.bak.*
.opencode/command/
.opencode/skills
.opencode/skills.bak
.opencode/skills.bak.*
.opencode/skills/
.vscode/mcp.json
/.mcp.json
/AGENTS.md
/AGENTS.md.bak
/CLAUDE.md
/CLAUDE.md.bak
/GEMINI.md
/GEMINI.md.bak
/opencode.json
AGENTS.md
AGENTS.md.bak.*
CLAUDE.md
CLAUDE.md.bak.*
GEMINI.md
GEMINI.md.bak.*
opencode.json
# END AI Agent Symlinks
.opencode/package-lock.json
99 changes: 99 additions & 0 deletions openspec/changes/init-user-template/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Design: Init User Template

## Technical Approach

Add a single `resolve_config_template()` function in `src/init.rs` as the resolution point for config template content. Thread the resolved string through `init()`, `init_wizard()`, and `build_default_config_with_skills_modes()` by changing their signatures. XDG discovery uses `std::env::var` — no new dependencies.

## Architecture Decisions

| Decision | Alternatives | Rationale |
|----------|-------------|-----------|
| Single `resolve_config_template()` fn in init.rs | Config method, TemplateResolver struct | It's a string lookup with fallback — a standalone fn is simplest. No state to manage. |
| No `dirs` crate for XDG | Add `dirs` or `directories` crate | `std::env::var("HOME")` + `XDG_CONFIG_HOME` is sufficient. Project already uses env vars directly. Windows users use `--template`. |
| Warn-and-fallback on invalid XDG file | Hard-fail on any invalid template | Explicit `--template` = user intent = hard-fail. Implicit XDG discovery = convenience = graceful degradation. |
| Pass `base_config: &str` to `build_default_config_with_skills_modes` | Global mutable state, config object | Minimal signature change, keeps function pure. |

## Data Flow

```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Markdown lint warnings across the OpenSpec documents.

  • openspec/changes/init-user-template/design.md#L18-L18: add a language to the code fence.
  • openspec/changes/init-user-template/design.md#L68-L68: add a blank line before the code fence.
  • openspec/changes/init-user-template/exploration.md#L86-L86: remove padding inside the code span.
  • openspec/changes/init-user-template/exploration.md#L92-L92: add a blank line after the heading.
  • openspec/changes/init-user-template/exploration.md#L97-L97: add a blank line after the heading.
  • openspec/changes/init-user-template/proposal.md#L11-L11: add a blank line after the heading.
  • openspec/changes/init-user-template/proposal.md#L21-L21: add a blank line after the heading.
  • openspec/changes/init-user-template/proposal.md#L30-L30: add a blank line after the heading.
  • openspec/changes/init-user-template/proposal.md#L33-L33: add a blank line after the heading.
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 18-18: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 3 files
  • openspec/changes/init-user-template/design.md#L18-L18 (this comment)
  • openspec/changes/init-user-template/exploration.md#L86-L86
  • openspec/changes/init-user-template/proposal.md#L11-L11
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/init-user-template/design.md` at line 18, Fix the Markdown
lint warnings in openspec/changes/init-user-template/design.md at lines 18 and
68 by adding a language to the code fence and a blank line before the fence; in
openspec/changes/init-user-template/exploration.md at lines 86, 92, and 97,
remove code-span padding and add blank lines after the headings; and in
openspec/changes/init-user-template/proposal.md at lines 11, 21, 30, and 33, add
blank lines after each heading.

Source: Linters/SAST tools

CLI --template flag
resolve_config_template(template_path)
├─ --template given? → read file → validate TOML → return content
├─ XDG file exists? → read file → validate (warn on fail) → return content
└─ neither? → return DEFAULT_CONFIG
config_content: String
┌────┴────┐
│ │
init() init_wizard()
│ │
│ build_default_config_with_skills_modes(base_config, modes)
│ │
▼ ▼
fs::write(config_path, content)
```

## File Changes

| File | Action | Description |
|------|--------|-------------|
| `src/main.rs` | Modify | Add `template: Option<PathBuf>` to `Init` struct (~line 334). Pass to `init()`/`init_wizard()` at lines 412-418. |
| `src/init.rs` | Modify | New `resolve_config_template()` + `resolve_user_config_path()` fns. Change `init()` (line 197): add `config_content: &str` param, replace `DEFAULT_CONFIG` at line 244. Change `build_default_config_with_skills_modes()` (line 1075): add `base_config: &str` param, replace `DEFAULT_CONFIG.lines()` at line 1079. Change `init_wizard()` (line 1583): add `template_path: Option<&Path>`, resolve template, thread to `init()` calls (lines 1667, 1686, 1695) and `build_default_config_with_skills_modes()` (line 1738). Same for `init_wizard_experimental_tui()`. |
| `website/docs/.../cli.mdx` | Modify | Document `--template` flag and XDG fallback behavior. |

## Interfaces / Contracts

```rust
/// Resolve config template content by precedence:
/// 1. Explicit --template path (hard error if invalid)
/// 2. XDG user config (warn + fallback if invalid)
/// 3. DEFAULT_CONFIG
pub fn resolve_config_template(template_path: Option<&Path>) -> Result<String>

/// Check XDG paths for user config. Returns None if nothing found.
fn resolve_user_config_path() -> Option<PathBuf>

// Changed signatures:
pub fn init(project_root: &Path, force: bool, config_content: &str) -> Result<()>
pub fn init_wizard(project_root: &Path, force: bool, template_path: Option<&Path>) -> Result<()>
pub fn init_wizard_experimental_tui(project_root: &Path, force: bool, template_path: Option<&Path>) -> Result<()>
fn build_default_config_with_skills_modes(base_config: &str, modes: &BTreeMap<String, SyncType>) -> String
```

XDG resolution logic:
```rust
fn resolve_user_config_path() -> Option<PathBuf> {
// 1. $XDG_CONFIG_HOME/agentsync/config.toml
// 2. $HOME/.config/agentsync/config.toml
// Returns None if neither exists
}
```

## Testing Strategy

| Layer | What to Test | Approach |
|-------|-------------|----------|
| Unit | `resolve_config_template()` — all 3 precedence cases | Temp files + env var override |
| Unit | `resolve_user_config_path()` — env var combinations | Set/unset `XDG_CONFIG_HOME`, `HOME` |
| Unit | `build_default_config_with_skills_modes()` with custom base | Pass non-default TOML, verify patching |
| Unit | Invalid TOML via `--template` → error | Assert anyhow error with context |
| Unit | Invalid XDG file → warn + fallback | Assert returns `DEFAULT_CONFIG` content |
| Integration | `init --template my.toml` | Temp dir, write template, run init, verify output |
| Integration | `init --wizard --template` | Verify wizard uses template as base |
| Integration | XDG auto-discovery | Set env, place file, run init without flag |
| Existing | `test_default_agents_md_contains_sections` | Unaffected — AGENTS.md unchanged |
| Existing | E2E `01-init-blank.sh` | Unaffected — no flag = same behavior |

## Migration / Rollout

No migration required. Purely additive feature — no flag means identical behavior to current code.

## Open Questions

- [x] Should `--template` work with `--wizard`? → YES, template becomes wizard base config
- [x] XDG file naming? → Fixed `config.toml`, any name via `--template`
- [ ] Should we validate template with full `Config::load` deserialization or just TOML parse? Recommendation: full `Config::load` to catch semantic errors (unknown agent names, invalid sync types).
113 changes: 113 additions & 0 deletions openspec/changes/init-user-template/exploration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Exploration: init --template and XDG user config

## Current State

`agentsync init` writes a hardcoded `DEFAULT_CONFIG` constant (src/init.rs:15, ~170 lines of TOML) containing all 7 agents enabled. Two code paths use it:

1. **`init()`** (init.rs:197) — plain init, writes `DEFAULT_CONFIG` directly at line 244: `fs::write(&config_path, DEFAULT_CONFIG)`
2. **`init_wizard()`** (init.rs:1583) — interactive wizard. Falls back to `init()` if no files found (line 1667) or no files selected (lines 1686, 1695). When it proceeds, it calls `build_default_config_with_skills_modes()` (line 1738) which iterates `DEFAULT_CONFIG.lines()` and patches `type =` values per agent. The wizard also appends a layout block via `upsert_agent_config_layout_block()`.
3. **`init_wizard_experimental_tui()`** — just shows a TUI intro then delegates to `init_wizard()`.

The `Init` command struct (main.rs:314) has 4 fields: `path`, `force`, `wizard`, `experimental_tui`. The handler (main.rs:398-426) branches on `wizard` flag.

## Affected Areas

- `src/main.rs:314-340` — Add `--template` arg to `Init` variant, pass it through handler (lines 398-419)
- `src/init.rs:197-261` — `init()` must accept optional template path, read+validate it, use instead of `DEFAULT_CONFIG`
- `src/init.rs:1075-1107` — `build_default_config_with_skills_modes()` takes `DEFAULT_CONFIG` as implicit base. Must accept a `&str` base parameter instead
- `src/init.rs:1583+` — `init_wizard()` calls `init()` as fallback (3 call sites: 1667, 1686, 1695) and `build_default_config_with_skills_modes()` at 1738. All need template threading
- `src/config.rs:290-298` — `Config::load()` already validates TOML → use it to validate user templates before writing
- `website/docs/src/content/docs/reference/cli.mdx:14-44` — Document `--template` flag and XDG fallback
- `Cargo.toml` — Possibly add `dirs` crate for XDG resolution

## Approaches

### 1. Minimal internal refactor — new `resolve_config_template()` function

- Add a `fn resolve_config_template(template: Option<&Path>) -> Result<String>` in init.rs that:
1. If `--template` given: read file, validate with `Config::load` (parse as TOML), return content
2. Else check `$XDG_CONFIG_HOME/agentsync/config.toml` then `~/.config/agentsync/config.toml`
3. Else return `DEFAULT_CONFIG.to_string()`
- Thread resolved template string into `init()` and `build_default_config_with_skills_modes()`
- Pros: Single resolution point, clear precedence, minimal API change
- Cons: None significant
- Effort: **Low-Medium**

### 2. Config struct method approach

- Put resolution logic on `Config` or a new `TemplateResolver` struct
- Pros: More testable in isolation
- Cons: Over-engineering for what's essentially a string lookup
- Effort: **Medium**

## Recommendation

**Approach 1**. The change is localized. Key implementation:

1. Add `template: Option<PathBuf>` to `Init` in main.rs
2. Create `resolve_config_template(template: Option<&Path>) -> Result<String>` in init.rs
3. Change `init(project_root, force)` signature to `init(project_root, force, config_content: &str)`
4. Change `build_default_config_with_skills_modes(modes)` to accept `base_config: &str` instead of reading `DEFAULT_CONFIG`
5. Thread through all 3 wizard fallback calls to `init()` and the direct `build_default_config_with_skills_modes()` call

### XDG Resolution — no `dirs` crate needed

`std::env::home_dir()` is deprecated since Rust 1.29 but still works. However, this project already uses `std::env::var("HOME")` patterns implicitly. The cleanest approach:

```rust
fn user_config_path() -> Option<PathBuf> {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
let p = PathBuf::from(xdg).join("agentsync/config.toml");
if p.exists() { return Some(p); }
}
if let Ok(home) = std::env::var("HOME") {
let p = PathBuf::from(home).join(".config/agentsync/config.toml");
if p.exists() { return Some(p); }
}
None
}
```

No new dependency needed. `$HOME` is reliable on macOS/Linux. Windows users would use `--template` explicitly.

### Validation strategy

Before writing the template content to disk, parse it with `toml::from_str::<Config>()` (same as `Config::load`). If it fails, error with: `"Invalid template: {path}: {toml_error}"`. This prevents writing broken configs.

### Output messaging

When using a template, change the init message:
- Default: `" ✔ Created: .agents/agentsync.toml"`
- Template: `" ✔ Created: .agents/agentsync.toml (from template: {path})"`
- XDG: `" ✔ Created: .agents/agentsync.toml (from user config: ~/.config/agentsync/config.toml)"`

## Risks

- **Wizard + template interaction**: The wizard's `build_default_config_with_skills_modes()` iterates lines looking for `[agents.X]` sections and `type = ` lines. If a user template has different agent names or structure, the skills mode patching will silently miss agents not in the template. This is actually **correct behavior** — it only patches what exists.
- **E2E test `01-init-blank.sh`**: Asserts specific agents exist in output (`[agents.claude]`, `[agents.gemini]`, `[agents.opencode]`). Won't break since default path is unchanged, but we need NEW tests for template path.
- **Backward compatibility**: Fully preserved — no flag = same behavior as today.

## Test Strategy

### Existing tests
- **E2E**: `tests/e2e/scenarios/01-init-blank.sh` (blank repo init), `02-init-adoption.sh` (wizard migration)
- **Unit**: `Config::load` tests in config.rs (lines 604-710) — file not found, invalid TOML, find_config precedence
- **No unit tests** for `init()`, `init_wizard()`, or `build_default_config_with_skills_modes()`

### New tests needed
1. **Unit: `resolve_config_template`** — test precedence: explicit path > XDG > default
2. **Unit: `resolve_config_template` with invalid TOML** — expect error
3. **Unit: `build_default_config_with_skills_modes` with custom base** — verify it patches a non-default template correctly
4. **Integration: `init` with `--template`** — write a 2-agent template, run init, verify output matches template
5. **Integration: XDG fallback** — set `XDG_CONFIG_HOME` env var, place config, run init without `--template`, verify it's picked up
6. **E2E**: New scenario `03-init-template.sh` — end-to-end with template flag

## Open Questions

1. **Should `--template` work with `--wizard`?** Recommended: YES — template becomes the base config that the wizard patches. The wizard's skills mode selection still works because it patches `type =` lines in whatever template is provided.
2. **Template file naming**: Should we require the file to be named `config.toml` at XDG path, or accept any name? Recommendation: Fixed name `config.toml` at XDG, any name via `--template`.
3. **Should we print which source was used?** Recommended: YES, always show provenance in output for debuggability.

## Ready for Proposal

Yes — the approach is clear, risks are low, backward compat is preserved. The orchestrator should proceed to sdd-propose with Approach 1.
78 changes: 78 additions & 0 deletions openspec/changes/init-user-template/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Proposal: Init User Template

## Intent

Users who manage multiple repos need consistent agent configs. Today, `agentsync init` always writes a hardcoded 7-agent default, forcing manual edits every time. This change lets users define their preferred config once and reuse it via `--template` flag or XDG auto-discovery.

GitHub issue: #478

## Scope

### In Scope
- `--template <path>` CLI flag on `agentsync init`
- XDG auto-discovery fallback (`$XDG_CONFIG_HOME/agentsync/config.toml` → `~/.config/agentsync/config.toml`)
- Precedence: `--template` > XDG user config > hardcoded `DEFAULT_CONFIG`
- Template validation before writing (parse as TOML, reject invalid)
- Provenance messaging in output (show which source was used)
- Template support in both `init` and `init --wizard` paths
- Unit + integration tests for precedence, validation, error cases
- Documentation updates (cli.mdx, getting-started.mdx, configuration.mdx)

### Out of Scope
- Partial/sparse config merge (template is full file replacement)
- `--default-agents` flag (covered by template mechanism)
- New crate dependencies (use `$HOME` + `$XDG_CONFIG_HOME` env vars directly)
- Windows XDG support (Windows users use `--template` explicitly)
- Template generation/scaffolding commands

## Capabilities

### New Capabilities
- `init-user-template`: User-level config template resolution for `agentsync init` — flag, XDG fallback, validation, and provenance output

### Modified Capabilities
- `config-schema`: No schema changes — templates must conform to existing schema. Validation reuses `Config::load` parsing.

## Approach

Add `resolve_config_template(template: Option<&Path>) -> Result<String>` in `init.rs` as the single resolution point. Thread the resolved string into `init()` and `build_default_config_with_skills_modes()` by changing their signatures to accept a `base_config: &str` parameter. XDG lookup uses `std::env::var` for `XDG_CONFIG_HOME` and `HOME` — no new dependencies.

## Affected Areas

| Area | Impact | Description |
|------|--------|-------------|
| `src/main.rs:314-340` | Modified | Add `template: Option<PathBuf>` to `Init` command struct |
| `src/init.rs:197-261` | Modified | `init()` accepts config content param instead of using `DEFAULT_CONFIG` |
| `src/init.rs:1075-1107` | Modified | `build_default_config_with_skills_modes()` accepts `base_config: &str` |
| `src/init.rs:1583+` | Modified | `init_wizard()` threads template through 3 fallback calls + direct build call |
| `src/init.rs` (new fn) | New | `resolve_config_template()` + `user_config_path()` |
| `tests/` | New | Unit tests for resolution, integration tests for `--template` and XDG |
| `website/docs/.../cli.mdx` | Modified | Document `--template` flag and XDG behavior |
| `website/docs/.../getting-started.mdx` | Modified | Mention template option |
| `website/docs/.../configuration.mdx` | Modified | Add user template section |

## Risks

| Risk | Likelihood | Mitigation |
|------|------------|------------|
| Wizard line-patching misses agents not in template | Low | Correct behavior — only patches what exists. Document this. |
| User provides syntactically valid but semantically broken template | Low | Validate with `Config::load` (full deserialization), not just TOML parse |
| XDG path doesn't exist on CI/containers | Low | Graceful fallback to `DEFAULT_CONFIG` — no error if XDG file absent |

## Rollback Plan

Revert the `--template` field from `Init` struct and restore original `init()`/`build_default_config_with_skills_modes()` signatures. No data migration needed — templates are read-only inputs, nothing is persisted beyond the generated `agentsync.toml` (which is always overwritable via `init --force`).

## Dependencies

- None. No new crates. Uses existing `Config::load` for validation.

## Success Criteria

- [ ] `agentsync init --template my.toml` writes config from template with provenance message
- [ ] `agentsync init` without flag picks up `~/.config/agentsync/config.toml` when present
- [ ] `agentsync init` without flag or XDG file behaves identically to current behavior
- [ ] `agentsync init --wizard --template my.toml` uses template as wizard base
- [ ] Invalid template produces clear error with file path and parse error
- [ ] All existing E2E tests pass unchanged
- [ ] New unit + integration tests cover precedence chain and error cases
Loading
Loading