Skip to content
Open
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ sce doctor # verify the install is healthy

`sce setup` writes OpenCode, Claude Code, and/or Pi config into your repo, installs the required git hooks, and initializes the per-repo Agent Trace database. Use `sce setup --pi` for Pi only, or `sce setup --all` for OpenCode + Claude Code + Pi. `sce doctor` is read-only by default; `sce doctor --fix` will repair the issues it knows how to repair (missing or stale hooks, missing canonical DB parent directories) and report the rest for manual follow-up.

With plain `sce setup`, after choosing targets and optional workflows, answer two independent confirmations:

```text
Enable automatic Agent Trace synchronization? [Y/n]
Enable SCE commit attribution trailers? [Y/n]
```

Both default to Yes, so pressing Enter enables that choice; answering `n` disables only the corresponding behavior. A successful target setup persists the answers in `.sce/config.json`:

```json
{
"agent_trace": { "auto_sync": true },
"policies": { "attribution_hooks": { "enabled": true } }
}
```

Non-interactive setup asks no questions. It explicitly enables both behaviors only when creating a missing repo-local config; an existing config's behavior values and omissions are not changed merely by a non-interactive run. Runtime precedence and hook behavior remain unchanged: `agent_trace.auto_sync` is config-file-only with omitted fallback `false`, while attribution can still be disabled by `SCE_ATTRIBUTION_HOOKS_DISABLED`, `SCE_DISABLED`, or its config value.

## Bash policy

**Stop agents from running commands your repo does not allow.**
Expand Down
13 changes: 3 additions & 10 deletions cli/src/services/config/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,6 @@ where
resolve_global_config_path,
)?;

if !runtime.validation_errors.is_empty() {
bail!(
"Agent Trace storage config resolution failed because a discovered config file is invalid: {}",
runtime.validation_errors.join(" | ")
);
}

Ok(ResolvedAgentTraceStorageRuntimeConfig {
repository_id: runtime.agent_trace_repository_id.value,
repository_remote: runtime.agent_trace_repository_remote.value,
Expand Down Expand Up @@ -564,7 +557,7 @@ where
source: ValueSource::ConfigFile(value.source),
},
None => ResolvedValue {
value: true,
value: false,
source: ValueSource::Default,
},
};
Expand Down Expand Up @@ -907,10 +900,10 @@ mod tests {
}

#[test]
fn agent_trace_auto_sync_defaults_to_true() {
fn agent_trace_auto_sync_defaults_to_false_when_missing() {
let runtime = resolve_runtime_with_config(None).unwrap();

assert!(runtime.agent_trace_auto_sync.value);
assert!(!runtime.agent_trace_auto_sync.value);
assert_eq!(runtime.agent_trace_auto_sync.source, ValueSource::Default);
}

Expand Down
29 changes: 23 additions & 6 deletions cli/src/services/setup/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ impl SetupCommand {
// The repository root is resolved before any prompt so the interactive
// optional-workflow prompt can pre-check the persisted selection.
let repository_root = resolve_setup_repository(&setup_start_path)?;
setup::validate_existing_repo_local_config(&repository_root).map_err(CliError::runtime)?;

let setup_dispatch = if self.request.context_only {
None
Expand All @@ -45,7 +44,14 @@ impl SetupCommand {
setup::SetupDispatch::Proceed {
mode: resolved_mode,
optional_workflows,
} => Some((resolved_mode, optional_workflows)),
agent_trace_auto_sync,
attribution_hooks_enabled,
} => Some((
resolved_mode,
optional_workflows,
agent_trace_auto_sync,
attribution_hooks_enabled,
)),
setup::SetupDispatch::Cancelled => {
return Ok(setup::setup_cancelled_text());
}
Expand Down Expand Up @@ -85,16 +91,27 @@ impl SetupCommand {
}

// Handle config target installation (OpenCode/Claude assets).
if let Some((resolved_mode, prompted_optional_workflows)) = setup_dispatch {
if let Some((
resolved_mode,
prompted_optional_workflows,
agent_trace_auto_sync,
attribution_hooks_enabled,
)) = setup_dispatch
{
// A prompted selection is authoritative for the run; without one the
// `--workflow` selection (or, absent that, the persisted one) applies.
let optional_workflows = prompted_optional_workflows
.as_deref()
.or(self.request.optional_workflows.as_deref());

let setup_message =
setup::run_setup_for_mode(&repository_root, resolved_mode, optional_workflows)
.map_err(CliError::runtime)?;
let setup_message = setup::run_setup_for_mode(
&repository_root,
resolved_mode,
optional_workflows,
agent_trace_auto_sync,
attribution_hooks_enabled,
)
.map_err(CliError::runtime)?;
sections.push(setup_message);
}

Expand Down
149 changes: 120 additions & 29 deletions cli/src/services/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ pub(crate) fn is_missing_git_remote_error(error: &anyhow::Error) -> bool {
}

/// Canonical JSON payload for a newly bootstrapped repo-local `.sce/config.json`.
/// Contains only the `$schema` declaration pointing to the SCE config JSON Schema.
/// Declares the SCE config JSON Schema and explicitly opts new repositories into
/// Agent Trace post-commit synchronization.
fn repo_local_config_bootstrap_payload() -> String {
format!(
"{{\n \"$schema\": \"{}\"\n}}\n",
"{{\n \"$schema\": \"{}\",\n \"agent_trace\": {{\n \"auto_sync\": true\n }},\n \"policies\": {{\n \"attribution_hooks\": {{\n \"enabled\": true\n }}\n }}\n}}\n",
crate::services::agent_trace::sce_config_schema_url()
)
}
Expand Down Expand Up @@ -229,6 +230,12 @@ pub enum SetupDispatch {
/// The optional workflows this run installs. `None` means no selection
/// was resolved here, so the persisted selection is reused downstream.
optional_workflows: Option<Vec<String>>,
/// The interactive selection for automatic Agent Trace synchronization.
/// `None` means setup did not prompt for this value.
agent_trace_auto_sync: Option<bool>,
/// The interactive selection for SCE commit-attribution trailers.
/// `None` means setup did not prompt for this value.
attribution_hooks_enabled: Option<bool>,
},
Cancelled,
}
Expand Down Expand Up @@ -400,6 +407,8 @@ pub fn run_setup_for_mode(
repository_root: &Path,
mode: SetupMode,
optional_workflows: Option<&[String]>,
agent_trace_auto_sync: Option<bool>,
attribution_hooks_enabled: Option<bool>,
) -> Result<String> {
let target = match mode {
SetupMode::Interactive => {
Expand All @@ -425,13 +434,19 @@ pub fn run_setup_for_mode(
})?;

// Persist selected integration targets and optional workflows in repo-local config.
persist_integration_targets(repository_root, target, &selected_optional_workflows)
.with_context(|| {
format!(
"Setup assets were installed for {} but failed to update repo-local config",
setup_target_label(target)
)
})?;
persist_integration_targets(
repository_root,
target,
&selected_optional_workflows,
agent_trace_auto_sync,
attribution_hooks_enabled,
)
.with_context(|| {
format!(
"Setup assets were installed for {} but failed to update repo-local config",
setup_target_label(target)
)
})?;

Ok(format_setup_install_success_message(&outcome))
}
Expand Down Expand Up @@ -485,27 +500,11 @@ pub fn ensure_git_remote(repository_root: &Path, remote_name: &str) -> Result<()
}))
}

/// Validates an existing repo-local `.sce/config.json` before setup performs
/// any other repository or lifecycle work. An absent config remains eligible
/// for the normal bootstrap path.
pub fn validate_existing_repo_local_config(repository_root: &Path) -> Result<()> {
let config_file = RepoPaths::new(repository_root).sce_config_file();
if !config_file.exists() {
return Ok(());
}

crate::services::config::validate_config_file(&config_file).with_context(|| {
format!(
"Setup preflight rejected invalid repo-local config file '{}'",
config_file.display()
)
})
}

/// Bootstraps the repo-local `.sce/config.json` file if it does not already exist.
///
/// Creates the `.sce/` parent directory as needed, then writes the canonical
/// schema-only JSON payload. If the file already exists, it is left untouched.
/// schema and explicit Agent Trace/attribution bootstrap JSON payload. If the
/// file already exists, it is left untouched.
pub fn bootstrap_repo_local_config(repository_root: &Path) -> Result<()> {
let repo_paths = RepoPaths::new(repository_root);
let config_file = repo_paths.sce_config_file();
Expand Down Expand Up @@ -841,10 +840,21 @@ pub fn persist_integration_targets(
repository_root: &Path,
target: SetupTarget,
selected_optional_workflows: &[String],
agent_trace_auto_sync: Option<bool>,
attribution_hooks_enabled: Option<bool>,
) -> Result<()> {
let repo_paths = RepoPaths::new(repository_root);
let config_file = repo_paths.sce_config_file();

// Default-discovered invalid config is intentionally degradable during
// setup. Do not rewrite it while recording the installed target: the
// startup resolver already reported the invalid layer and setup must leave
// the user's file byte-for-byte unchanged.
if config_file.exists() && crate::services::config::validate_config_file(&config_file).is_err()
{
return Ok(());
}

// Read existing config or start with bootstrap payload.
let raw = if config_file.exists() {
fs::read_to_string(&config_file)
Expand Down Expand Up @@ -901,6 +911,37 @@ pub fn persist_integration_targets(
}),
);

if let Some(value) = agent_trace_auto_sync {
let agent_trace = config_obj.entry("agent_trace").or_insert_with(|| json!({}));
let agent_trace_obj = agent_trace.as_object_mut().with_context(|| {
format!(
"Config file '{}' must contain an object at 'agent_trace'.",
config_file.display()
)
})?;
agent_trace_obj.insert("auto_sync".to_string(), json!(value));
}

if let Some(value) = attribution_hooks_enabled {
let policies = config_obj.entry("policies").or_insert_with(|| json!({}));
let policies_obj = policies.as_object_mut().with_context(|| {
format!(
"Config file '{}' must contain an object at 'policies'.",
config_file.display()
)
})?;
let attribution_hooks = policies_obj
.entry("attribution_hooks")
.or_insert_with(|| json!({}));
let attribution_hooks_obj = attribution_hooks.as_object_mut().with_context(|| {
format!(
"Config file '{}' must contain an object at 'policies.attribution_hooks'.",
config_file.display()
)
})?;
attribution_hooks_obj.insert("enabled".to_string(), json!(value));
}

let updated = serde_json::to_string_pretty(&config).with_context(|| {
format!(
"Failed to serialize updated config for '{}'",
Expand Down Expand Up @@ -1617,6 +1658,14 @@ pub trait SetupTargetPrompter {
/// The optional workflows to install, pre-checked from `defaults`.
/// `None` means the operator cancelled the prompt.
fn prompt_optional_workflows(&self, defaults: &[String]) -> Result<Option<Vec<String>>>;

/// Whether automatic Agent Trace synchronization should be enabled.
/// `None` means the operator cancelled the prompt.
fn prompt_agent_trace_auto_sync(&self) -> Result<Option<bool>>;

/// Whether SCE commit-attribution trailers should be enabled.
/// `None` means the operator cancelled the prompt.
fn prompt_attribution_hooks_enabled(&self) -> Result<Option<bool>>;
}

#[derive(Clone, Copy, Debug, Default)]
Expand All @@ -1630,6 +1679,14 @@ impl SetupTargetPrompter for InquireSetupTargetPrompter {
fn prompt_optional_workflows(&self, defaults: &[String]) -> Result<Option<Vec<String>>> {
prompt::prompt_optional_workflows(defaults)
}

fn prompt_agent_trace_auto_sync(&self) -> Result<Option<bool>> {
prompt::prompt_agent_trace_auto_sync()
}

fn prompt_attribution_hooks_enabled(&self) -> Result<Option<bool>> {
prompt::prompt_attribution_hooks_enabled()
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -1666,7 +1723,7 @@ fn setup_prompt_title_with_color_policy(color_enabled: bool) -> String {

mod prompt {
use anyhow::{bail, Result};
use inquire::{InquireError, MultiSelect, Select};
use inquire::{Confirm, InquireError, MultiSelect, Select};

use crate::services::style::{
prompt_label, prompt_label_with_color_policy, prompt_value_with_color_policy,
Expand All @@ -1678,6 +1735,8 @@ mod prompt {
SetupDispatch::Proceed {
mode: SetupMode::NonInteractive(target),
optional_workflows: None,
agent_trace_auto_sync: None,
attribution_hooks_enabled: None,
}
}

Expand Down Expand Up @@ -1737,6 +1796,25 @@ mod prompt {
}
}

pub(super) fn prompt_agent_trace_auto_sync() -> Result<Option<bool>> {
prompt_confirmation("Enable automatic Agent Trace synchronization?")
}

pub(super) fn prompt_attribution_hooks_enabled() -> Result<Option<bool>> {
prompt_confirmation("Enable SCE commit attribution trailers?")
}

fn prompt_confirmation(label: &str) -> Result<Option<bool>> {
match Confirm::new(label).with_default(true).prompt() {
Ok(value) => Ok(Some(value)),
Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => Ok(None),
Err(InquireError::NotTTY) => bail!(
"Interactive setup requires a TTY. Re-run with '--non-interactive' and one of '--opencode', '--claude', '--pi', '--codex', or '--all'."
),
Err(error) => Err(error.into()),
}
}

/// One selectable row per optional workflow, in catalog order.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct OptionalWorkflowRow {
Expand Down Expand Up @@ -1870,14 +1948,27 @@ where
return Ok(SetupDispatch::Cancelled);
};

let Some(agent_trace_auto_sync) = prompter.prompt_agent_trace_auto_sync()? else {
return Ok(SetupDispatch::Cancelled);
};

let Some(attribution_hooks_enabled) = prompter.prompt_attribution_hooks_enabled()?
else {
return Ok(SetupDispatch::Cancelled);
};

Ok(SetupDispatch::Proceed {
mode,
optional_workflows: Some(optional_workflows),
agent_trace_auto_sync: Some(agent_trace_auto_sync),
attribution_hooks_enabled: Some(attribution_hooks_enabled),
})
}
SetupMode::NonInteractive(target) => Ok(SetupDispatch::Proceed {
mode: SetupMode::NonInteractive(target),
optional_workflows: None,
agent_trace_auto_sync: None,
attribution_hooks_enabled: None,
}),
}
}
Expand Down Expand Up @@ -1956,7 +2047,7 @@ mod tests {
assert_eq!(
payload,
format!(
"{{\n \"$schema\": \"https://sce.crocoder.dev/v{}/config.json\"\n}}\n",
"{{\n \"$schema\": \"https://sce.crocoder.dev/v{}/config.json\",\n \"agent_trace\": {{\n \"auto_sync\": true\n }},\n \"policies\": {{\n \"attribution_hooks\": {{\n \"enabled\": true\n }}\n }}\n}}\n",
env!("CARGO_PKG_VERSION")
)
);
Expand Down
Loading
Loading