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
32 changes: 31 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,37 @@
"Skill(commit-commands:commit)",
"Bash(cat)",
"Read(//tmp/**)",
"Skill(commit-commands:commit-push-pr)"
"Skill(commit-commands:commit-push-pr)",
"Bash(git tag -a v0.4.0 9a46e86 -m 'Release v0.4.0 *)",
"Bash(gh release *)",
"Bash(git tag *)",
"Bash(./target/release/rivet validate *)",
"Bash(./target/release/rivet stats *)",
"Bash(./target/release/rivet coverage *)",
"Bash(./target/release/rivet commits *)",
"Bash(awk -F'|' '{printf \"%-10s %-60s %s\\\\n\", substr\\($1,1,8\\), substr\\($2,1,60\\), substr\\($3,1,80\\)}')",
"Bash(./target/release/rivet list *)",
"Bash(awk '{print $6, $7, $8, $NF}')",
"Bash(awk 'BEGIN{job=\"\"} /:$/{if\\($0!~/error/\\){job=$0}} /continue-on-error: true/{print job}')",
"Bash(cargo mutants *)",
"Bash(curl -s \"https://raw.githubusercontent.com/pulseengine/rules_verus/e2c1600a8cca4c0deb78c5fcb4a33f1da2273d29/verus/BUILD.bazel\")",
"Bash(curl -s \"https://raw.githubusercontent.com/pulseengine/rules_verus/e2c1600a8cca4c0deb78c5fcb4a33f1da2273d29/verus/extensions.bzl\")",
"Bash(git ls-remote *)",
"Bash(awk -F'\\\\t' '{print $1,$4}')",
"Bash(awk -F'\\\\t' '{print $2}')",
"WebFetch(domain:arxiv.org)",
"Bash(pdftotext /Users/r/.claude/projects/-Users-r-git-pulseengine-rivet/b8aa1c86-f679-4617-b1b6-9173ce3de7fc/tool-results/webfetch-1776711947091-wbamv0.pdf /tmp/paper.txt)",
"Bash(pip install *)",
"Bash(/opt/homebrew/bin/python3.11 -c ' *)",
"Bash(awk -F'\\\\t' '{printf \"%-30s %s\\\\n\",$1,$2}')",
"Bash(awk -F'\\\\t' '{printf \"%-35s %s\\\\n\",$1,$2}')",
"Bash(awk -F'\\\\t' '$2==\"fail\"{print $1}')",
"Bash(awk -F'\\\\t' '$2==\"fail\"{print $1,$4}')",
"Bash(awk -F'\\\\t' '{printf \"%-35s %-5s %s\\\\n\",$1,$2,$4}')",
"Bash(cargo tree *)",
"Bash(git restore *)",
"Bash(awk -F'\\\\t' '{print $4}')",
"Bash(awk -F'\\\\t' '{print $1, $2}')"
]
}
}
19 changes: 16 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
<!-- Auto-generated by `rivet init --agents`. Re-run to update after artifact changes. -->
<!-- This file has two kinds of content:

1. A rivet-managed section (between the BEGIN/END rivet-managed markers
below) that `rivet init --agents` regenerates from your project state.
Do not edit inside that region — changes are overwritten.

2. Everything outside the markers, which rivet never touches. The
hand-authored Commit Traceability reference and the retroactive
traceability map live below; they survive regeneration. -->

<!-- BEGIN rivet-managed: auto-generated section. Edits between these markers will be overwritten on `rivet init --agents`. -->
> NOTE: This section is auto-generated by `rivet init --agents`. Do not edit between the `BEGIN rivet-managed` / `END rivet-managed` markers — edits there are overwritten on regeneration. Content outside the markers is preserved.

# AGENTS.md — Rivet Project Instructions

> This file was generated by `rivet init --agents`. Re-run the command
> any time artifacts change to keep this file current.
> This section was generated by `rivet init --agents`. Re-run the command
> any time artifacts change to keep it current.

## Project Overview

Expand Down Expand Up @@ -105,6 +117,7 @@ Use `rivet validate --format json` for machine-readable output.
- Use `rivet add` to create artifacts (auto-generates next ID)
- Always include traceability links when creating artifacts
- Run `rivet validate` before committing
<!-- END rivet-managed -->

## Commit Traceability

Expand Down
19 changes: 19 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
<!-- This file has two kinds of content:
1. A rivet-managed section (between the BEGIN/END rivet-managed markers
below) that `rivet init --agents` regenerates from your project state.
Do not edit inside that region — changes are overwritten.
2. Everything outside the markers, which rivet never touches. The
Claude Code / human-audience instructions below live here so they
survive regeneration. -->

# CLAUDE.md

See [AGENTS.md](AGENTS.md) for project instructions.
Expand Down Expand Up @@ -49,3 +59,12 @@ and retroactive traceability map.
## AI Provenance
- AI provenance is auto-stamped via PostToolUse hook when artifact files are edited
- When manually stamping, include model: `rivet stamp <ID> --created-by ai-assisted --model claude-opus-4-6`

<!-- BEGIN rivet-managed: auto-generated section. Edits between these markers will be overwritten on `rivet init --agents`. -->
> NOTE: This section is auto-generated by `rivet init --agents`. Do not edit between the `BEGIN rivet-managed` / `END rivet-managed` markers — edits there are overwritten on regeneration. Content outside the markers is preserved.
<!-- The managed region starts empty in the committed tree; running
`rivet init --agents` will populate it with a CLAUDE.md shim
pointing at AGENTS.md plus any Claude-Code-specific hints derived
from rivet.yaml. The hand-authored sections above are preserved. -->
<!-- END rivet-managed -->
243 changes: 211 additions & 32 deletions rivet-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,18 @@ enum Command {
#[arg(long)]
agents: bool,

/// With --agents: wrap existing AGENTS.md/CLAUDE.md content with
/// rivet-managed markers (the generated section goes on top, the
/// previous content is preserved verbatim below).
#[arg(long, requires = "agents")]
migrate: bool,

/// With --agents: overwrite existing AGENTS.md/CLAUDE.md even if
/// they have no rivet-managed markers. DESTRUCTIVE — replaces the
/// whole file. Prefer --migrate when possible.
#[arg(long, requires = "agents")]
force_regen: bool,

/// Install git hooks (commit-msg, pre-commit) that call rivet for validation
#[arg(long)]
hooks: bool,
Expand Down Expand Up @@ -862,11 +874,13 @@ fn run(cli: Cli) -> Result<bool> {
schema,
dir,
agents,
migrate,
force_regen,
hooks,
} = &cli.command
{
if *agents {
return cmd_init_agents(&cli);
return cmd_init_agents(&cli, *migrate, *force_regen);
}
if *hooks {
return cmd_init_hooks(dir);
Expand Down Expand Up @@ -2548,7 +2562,20 @@ fn sanitize_for_table(s: &str) -> String {
}

/// Generate AGENTS.md (and CLAUDE.md shim) from current project state.
fn cmd_init_agents(cli: &Cli) -> Result<bool> {
///
/// The generated content is wrapped in `rivet-managed` HTML-comment markers
/// so that manual edits made outside the markers survive regeneration. See
/// [`rivet_core::managed_section`] for the splice semantics.
///
/// Behaviour on an existing file:
/// - Has exactly one marker pair: splice — replace only the managed region.
/// - Has no markers and `migrate` is true: wrap existing content (managed
/// section goes on top, prior content preserved verbatim below).
/// - Has no markers and `force_regen` is true: overwrite the whole file
/// with a freshly markered version (destructive; loud warning printed).
/// - Has no markers and neither flag is set: refuse with exit code 1.
/// - Has multiple marker pairs: refuse with exit code 1 (ambiguous).
fn cmd_init_agents(cli: &Cli, migrate: bool, force_regen: bool) -> Result<bool> {
let config_path = cli.project.join("rivet.yaml");

// Try to load project config — it's okay if it doesn't exist
Expand Down Expand Up @@ -2703,13 +2730,17 @@ fn cmd_init_agents(cli: &Cli) -> Result<bool> {
String::new()
};

// Build the AGENTS.md content
let agents_md = format!(
r#"<!-- Auto-generated by `rivet init --agents`. Re-run to update after artifact changes. -->
// Build the managed body of AGENTS.md. This is what goes *between* the
// BEGIN/END rivet-managed markers; markers themselves are added by
// `managed_section::wrap_fresh` / `splice_managed_section`.
let sentinel = rivet_core::managed_section::MANAGED_SENTINEL;
let agents_managed = format!(
r#"{sentinel}

# AGENTS.md — Rivet Project Instructions

> This file was generated by `rivet init --agents`. Re-run the command
> any time artifacts change to keep this file current.
> This section was generated by `rivet init --agents`. Re-run the command
> any time artifacts change to keep it current.

## Project Overview

Expand Down Expand Up @@ -2765,41 +2796,66 @@ Use `rivet validate --format json` for machine-readable output.
{commits_section}"#
);

// Write AGENTS.md (always regenerate — reflects current project state)
// Preamble written above the managed section ONLY when the file is
// fresh. Users can edit this freely; rivet never rewrites it.
let agents_preamble = "\
<!-- This file has two kinds of content:

1. A rivet-managed section (between the BEGIN/END rivet-managed markers
below) that `rivet init --agents` regenerates from your project state.
Do not edit inside that region — changes are overwritten.

2. Everything outside the markers, which rivet never touches. Add your
own sections, audit notes, and project-specific guidance freely,
above or below the managed region. -->

";

// Write AGENTS.md using managed-section splice semantics.
let agents_path = cli.project.join("AGENTS.md");
let agents_verb = if agents_path.exists() {
"updated"
write_managed_file(
&agents_path,
&agents_managed,
agents_preamble,
migrate,
force_regen,
)?;

// Write CLAUDE.md. It's a short shim pointing at AGENTS.md plus
// Claude-Code-specific hints. Same marker semantics apply.
let claude_trailer_line = if config.commits.is_some() {
"- Commit messages require artifact trailers (Implements/Fixes/Verifies/Satisfies/Refs)\n"
} else {
"created"
""
};
std::fs::write(&agents_path, &agents_md)
.with_context(|| format!("writing {}", agents_path.display()))?;
println!(" {agents_verb} {}", agents_path.display());
let claude_managed = format!(
"\
{sentinel}

// Generate CLAUDE.md shim if it doesn't already exist
let claude_path = cli.project.join("CLAUDE.md");
if !claude_path.exists() {
let trailer_line = if config.commits.is_some() {
"- Commit messages require artifact trailers (Implements/Fixes/Verifies/Satisfies/Refs)\n"
} else {
""
};
let claude_md = format!(
r#"# CLAUDE.md
# CLAUDE.md

See [AGENTS.md](AGENTS.md) for project instructions.

Additional Claude Code settings:
- Use `rivet validate` to verify changes to artifact YAML files
- Use `rivet list --format json` for machine-readable artifact queries
{trailer_line}"#
);
std::fs::write(&claude_path, &claude_md)
.with_context(|| format!("writing {}", claude_path.display()))?;
println!(" created {}", claude_path.display());
} else {
println!(" CLAUDE.md already exists, skipping");
}
{claude_trailer_line}",
);
let claude_preamble = "\
<!-- Like AGENTS.md, this file splits into a rivet-managed region (auto
regenerated by `rivet init --agents`) and free-form content outside
the markers (preserved across regenerations). Put any Claude-Code
specific hand-authored guidance outside the markers. -->

";
let claude_path = cli.project.join("CLAUDE.md");
write_managed_file(
&claude_path,
&claude_managed,
claude_preamble,
migrate,
force_regen,
)?;

println!(
"\nGenerated AGENTS.md for project '{}' ({} artifacts, {} types)",
Expand All @@ -2809,6 +2865,129 @@ Additional Claude Code settings:
Ok(true)
}

/// Write a managed file using splice semantics.
///
/// Rules, in priority order:
/// 1. File does not exist: write `preamble + wrap_fresh(managed_body)`.
/// 2. File exists with exactly one marker pair: splice.
/// 3. File exists without markers, `--migrate`: wrap existing content.
/// 4. File exists without markers, `--force-regen`: overwrite (warn loudly).
/// 5. File exists without markers, no flag: refuse with `anyhow::bail!`.
/// 6. File has multiple marker pairs (or other structural problems):
/// refuse with the underlying error message.
fn write_managed_file(
path: &std::path::Path,
managed_body: &str,
fresh_preamble: &str,
migrate: bool,
force_regen: bool,
) -> Result<()> {
use rivet_core::managed_section::{
self, ManagedSectionError, has_markers, migrate_wrap, splice_managed_section, wrap_fresh,
};

let file_label = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string());

if !path.exists() {
// Fresh file: write preamble + markered block.
let mut out = String::new();
out.push_str(fresh_preamble);
out.push_str(&wrap_fresh(managed_body));
std::fs::write(path, out).with_context(|| format!("writing {}", path.display()))?;
println!(" created {}", path.display());
return Ok(());
}

let existing =
std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;

// Fast-path detection so error precedence matches the design doc:
// `--migrate` only applies when the file has no markers at all; if
// markers exist we always splice (or surface a multi-marker error).
if !has_markers(&existing) {
if migrate {
let out = migrate_wrap(&existing, managed_body);
std::fs::write(path, out).with_context(|| format!("writing {}", path.display()))?;
println!(
" migrated {} (wrapped existing content; managed section now on top, prior content preserved below)",
path.display()
);
return Ok(());
}
if force_regen {
eprintln!(
"warning: --force-regen: overwriting {} with freshly markered content. Any existing content in this file is being discarded.",
path.display()
);
let mut out = String::new();
out.push_str(fresh_preamble);
out.push_str(&wrap_fresh(managed_body));
std::fs::write(path, out).with_context(|| format!("writing {}", path.display()))?;
println!(" force-regenerated {}", path.display());
return Ok(());
}
anyhow::bail!(
"{file_label} exists without rivet-managed markers. Refusing to overwrite and destroy existing content.\n\
Choose one:\n\
* rivet init --agents --migrate (safe: wraps existing content below a fresh managed section)\n\
* rivet init --agents --force-regen (destructive: replaces the whole file)\n\
* manually wrap the auto-generated portion with:\n\
{begin}\n\
...managed content...\n\
{end}\n\
then re-run `rivet init --agents`.",
begin = managed_section::BEGIN_MARKER,
end = managed_section::END_MARKER,
);
}

// File has at least one BEGIN marker — splice (or report structural error).
match splice_managed_section(&existing, managed_body) {
Ok(new_content) => {
std::fs::write(path, new_content)
.with_context(|| format!("writing {}", path.display()))?;
println!(
" updated {} (managed section only; other content preserved)",
path.display()
);
Ok(())
}
Err(ManagedSectionError::MultipleBeginMarkers(lines)) => {
let lines_str = lines
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!(
"{file_label} has multiple rivet-managed BEGIN markers (lines: {lines_str}). \
Refusing to choose which pair to splice. Delete the extras and re-run."
);
}
Err(ManagedSectionError::UnclosedMarker { begin_line }) => {
anyhow::bail!(
"{file_label}: BEGIN rivet-managed marker at line {begin_line} has no matching END marker. \
Close it with `{end}` and re-run.",
end = managed_section::END_MARKER,
);
}
Err(ManagedSectionError::OrphanEndMarker { end_line }) => {
anyhow::bail!(
"{file_label}: END rivet-managed marker at line {end_line} appears before any BEGIN marker."
);
}
Err(ManagedSectionError::NoMarkers) => {
// Shouldn't reach here because we checked `has_markers` above,
// but handle defensively in case the definitions diverge.
anyhow::bail!(
"{file_label}: internal error — has_markers reported true but splice found none"
);
}
}
}

/// Load STPA files directly and validate them.
fn cmd_stpa(
stpa_dir: &std::path::Path,
Expand Down
Loading
Loading