Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4816339
feat: add qa.md skill to embedded command resources
dollspace-gay Mar 26, 2026
09c09e5
Merge pull request #524 from forecast-bio/feature/add-qa-skill-v2
dollspace-gay Mar 26, 2026
672d0aa
fix: full-codebase QA audit — 180+ fixes across security, correctness…
dollspace-gay Mar 26, 2026
5e79416
Merge pull request #527 from forecast-bio/qa/audit-fixes-v2
dollspace-gay Mar 26, 2026
a2e1f49
Merge branch 'develop' of github.com:forecast-bio/crosslink into develop
maxine-at-forecast Mar 27, 2026
d32e1bf
feat: add first-class Shell/Bash support to rules, detection, and hooks
Mar 30, 2026
1c9b16d
Merge pull request #531 from Viscosity4373/feature/shell-support
dollspace-gay Mar 30, 2026
e2e7ef5
feat: add crosslink init --update with manifest-tracked safe upgrades
dollspace-gay Mar 30, 2026
54b1b2f
fix: resolve clippy pedantic and nursery warnings across codebase
dollspace-gay Mar 30, 2026
082cfe1
ci: retrigger with clean cache
dollspace-gay Mar 30, 2026
4c7309d
Merge pull request #534 from forecast-bio/feature/init-update-manifest
dollspace-gay Mar 30, 2026
a5eb7dc
docs: document team and solo configuration presets
dollspace-gay Mar 30, 2026
4cbeb48
Merge pull request #535 from forecast-bio/docs/team-solo-presets
dollspace-gay Mar 30, 2026
be83951
fix: consistent signing bypass for all hub-cache commits
dollspace-gay Mar 30, 2026
0b2469e
Merge pull request #536 from forecast-bio/fix/consistent-signing-bypass
dollspace-gay Mar 30, 2026
18ad851
fix: gitignore .hub-write-lock to prevent recovery commit loop
dollspace-gay Mar 30, 2026
b07910d
Merge pull request #537 from forecast-bio/fix/hub-write-lock-gitignore
dollspace-gay Mar 30, 2026
9413cfc
fix: add gh to allowed bash prefixes and cache session status in hook
dollspace-gay Mar 30, 2026
bab3202
Merge pull request #538 from forecast-bio/fix/allowed-bash-and-sessio…
dollspace-gay Mar 30, 2026
4a366bc
fix: add --base flag to swarm merge for repos without develop branch
dollspace-gay Mar 30, 2026
4dc1624
Merge pull request #539 from forecast-bio/fix/swarm-merge-base-branch
dollspace-gay Mar 30, 2026
a74b52a
Merge branch 'develop' of github.com:forecast-bio/crosslink into develop
maxine-at-forecast Mar 30, 2026
2ec520c
release: prepare v0.7.0 — bump version, update CHANGELOG, fix smoke t…
maxine-at-forecast Mar 31, 2026
491c31f
style: cargo fmt on smoke test files
maxine-at-forecast Mar 31, 2026
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

## [0.7.0] - 2026-03-30

### Added
- `crosslink init --update` with manifest-tracked safe upgrades — tracks installed resource versions and applies incremental updates without overwriting user customizations
- First-class Shell/Bash support in language rules, detection, and hooks
- QA architectural review skill (`/qa`) shipped with `crosslink init`
- Team and solo configuration preset documentation

### Fixed
- Full-codebase QA audit — 180+ fixes across security, correctness, and architecture: shell injection, fail-open hooks, CORS, transaction safety, hydration data loss, non-atomic writes, TOCTOU races, N+1 queries, and structural refactors (init.rs split, config registry extraction, `status.rs` → `lifecycle.rs`)
- `swarm merge --base` flag for repos without a `develop` branch
- `gh` added to allowed bash prefixes; session status caching in work-check hook
- `.hub-write-lock` excluded from git tracking to prevent recovery commit loop
- Consistent signing bypass for all hub-cache commits
- Resolved clippy pedantic and nursery warnings across codebase

### Changed
- `init.rs` split into `init/mod.rs`, `init/merge.rs`, `init/python.rs`, `init/signing.rs`, `init/walkthrough.rs` for maintainability
- Config command logic extracted to `config_registry.rs`
- `status.rs` renamed to `lifecycle.rs`
- Shared error helpers module added to server (`server/errors.rs`)
- TUI tabs refactored with shared helpers to reduce duplication

## [0.6.0] - 2026-03-24

### Added
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,28 @@ Research done by one agent is available to all.
- **Auto-injection** — Relevant knowledge pages injected into agent context via MCP server
- **Conflict resolution** — Accept-both merge strategy for concurrent knowledge edits

### Configuration Presets

Get the right defaults for your workflow without reading docs.

- **Team mode** — Strict tracking, required comments, CI verification, enforced commit signing. For shared repos with multiple contributors or agents.
- **Solo mode** — Relaxed tracking, encouraged comments, local-only verification, signing disabled. For personal projects and solo development.
- **Custom** — Configure each setting individually via the interactive walkthrough.

```bash
# Choose during first-time setup (interactive TUI)
crosslink init

# Or apply a preset directly
crosslink config --preset team
crosslink config --preset solo

# Skip the TUI and use team defaults
crosslink init --defaults
```

The presets configure tracking strictness, comment discipline, lock stealing policy, kickoff verification level, and signing enforcement. Run `crosslink config show` to see current settings, or `crosslink config --reconfigure` to re-run the walkthrough.

### Behavioral Hooks & Rules

Your agents follow the rules without being told.
Expand Down
60 changes: 30 additions & 30 deletions crosslink/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crosslink/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "crosslink"
version = "0.6.0"
version = "0.7.0"
edition = "2021"
rust-version = "1.87"
authors = ["dollspace-gay", "Maxine Levesque <hello@maxine.science>", "Forecast Analytical <analytical@forecast.bio>"]
Expand Down
45 changes: 24 additions & 21 deletions crosslink/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Build script to track include_str! dependencies, inject git metadata,
//! Build script to track `include_str`! dependencies, inject git metadata,
//! and auto-generate rule file includes from resources/crosslink/rules/.

use std::fs;
Expand All @@ -25,7 +25,7 @@ fn main() {
} else {
format!("{}+{}", env!("CARGO_PKG_VERSION"), hash)
};
println!("cargo:rustc-env=CROSSLINK_VERSION={}", suffix);
println!("cargo:rustc-env=CROSSLINK_VERSION={suffix}");
}
}

Expand All @@ -48,7 +48,7 @@ fn main() {
let rules_dir = Path::new("resources/crosslink/rules");
if rules_dir.is_dir() {
if let Err(e) = generate_rules_file(rules_dir) {
eprintln!("cargo:warning=Failed to generate rules_gen.rs: {}", e);
eprintln!("cargo:warning=Failed to generate rules_gen.rs: {e}");
}
}

Expand All @@ -57,7 +57,7 @@ fn main() {
let commands_dir = Path::new("resources/claude/commands");
if commands_dir.is_dir() {
if let Err(e) = generate_commands_file(commands_dir) {
eprintln!("cargo:warning=Failed to generate commands_gen.rs: {}", e);
eprintln!("cargo:warning=Failed to generate commands_gen.rs: {e}");
}
}
}
Expand All @@ -66,22 +66,22 @@ fn generate_commands_file(commands_dir: &Path) -> Result<(), Box<dyn std::error:
let mut cmd_entries: Vec<(String, String)> = Vec::new();

let mut entries: Vec<_> = fs::read_dir(commands_dir)?
.filter_map(|e| e.ok())
.filter_map(std::result::Result::ok)
.filter(|e| e.file_name().to_string_lossy().ends_with(".md"))
.collect();
entries.sort_by_key(|e| e.file_name());
entries.sort_by_key(std::fs::DirEntry::file_name);

for entry in entries {
let filename = entry.file_name().to_string_lossy().to_string();
let rel_path = format!("resources/claude/commands/{}", filename);
println!("cargo:rerun-if-changed={}", rel_path);
let rel_path = format!("resources/claude/commands/{filename}");
println!("cargo:rerun-if-changed={rel_path}");

// Generate a const name: crosslink-guide.md -> CMD_CROSSLINK_GUIDE
let const_name = filename
.trim_end_matches(".md")
.to_uppercase()
.replace('-', "_");
let const_name = format!("CMD_{}", const_name);
let const_name = format!("CMD_{const_name}");

cmd_entries.push((filename, const_name));
}
Expand All @@ -107,8 +107,7 @@ fn generate_commands_file(commands_dir: &Path) -> Result<(), Box<dyn std::error:
let abs_path_str = abs_path.to_string_lossy().replace('\\', "/");
writeln!(
gen_file,
"pub(crate) const {}: &str = include_str!(\"{}\");",
const_name, abs_path_str
"pub(crate) const {const_name}: &str = include_str!(\"{abs_path_str}\");"
)?;
}

Expand All @@ -118,7 +117,7 @@ fn generate_commands_file(commands_dir: &Path) -> Result<(), Box<dyn std::error:
"pub(crate) const COMMAND_FILES: &[(&str, &str)] = &["
)?;
for (filename, const_name) in &cmd_entries {
writeln!(gen_file, " (\"{}\", {}),", filename, const_name)?;
writeln!(gen_file, " (\"{filename}\", {const_name}),")?;
}
writeln!(gen_file, "];")?;

Expand All @@ -129,26 +128,31 @@ fn generate_rules_file(rules_dir: &Path) -> Result<(), Box<dyn std::error::Error
let mut rule_entries: Vec<(String, String)> = Vec::new();

let mut entries: Vec<_> = fs::read_dir(rules_dir)?
.filter_map(|e| e.ok())
.filter_map(std::result::Result::ok)
.filter(|e| {
let name = e.file_name().to_string_lossy().to_string();
name.ends_with(".md") || name.ends_with(".txt")
std::path::Path::new(&name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
|| std::path::Path::new(&name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("txt"))
})
.collect();
entries.sort_by_key(|e| e.file_name());
entries.sort_by_key(std::fs::DirEntry::file_name);

for entry in entries {
let filename = entry.file_name().to_string_lossy().to_string();
let rel_path = format!("resources/crosslink/rules/{}", filename);
println!("cargo:rerun-if-changed={}", rel_path);
let rel_path = format!("resources/crosslink/rules/{filename}");
println!("cargo:rerun-if-changed={rel_path}");

// Generate a const name from filename: quality.md -> RULE_QUALITY
let const_name = filename
.trim_end_matches(".md")
.trim_end_matches(".txt")
.to_uppercase()
.replace('-', "_");
let const_name = format!("RULE_{}", const_name);
let const_name = format!("RULE_{const_name}");

rule_entries.push((filename, const_name));
}
Expand Down Expand Up @@ -177,8 +181,7 @@ fn generate_rules_file(rules_dir: &Path) -> Result<(), Box<dyn std::error::Error
let abs_path_str = abs_path.to_string_lossy().replace('\\', "/");
writeln!(
gen_file,
"pub(crate) const {}: &str = include_str!(\"{}\");",
const_name, abs_path_str
"pub(crate) const {const_name}: &str = include_str!(\"{abs_path_str}\");"
)?;
}

Expand All @@ -190,7 +193,7 @@ fn generate_rules_file(rules_dir: &Path) -> Result<(), Box<dyn std::error::Error
"pub(crate) const RULE_FILES: &[(&str, &str)] = &["
)?;
for (filename, const_name) in &rule_entries {
writeln!(gen_file, " (\"{}\", {}),", filename, const_name)?;
writeln!(gen_file, " (\"{filename}\", {const_name}),")?;
}
writeln!(gen_file, "];")?;

Expand Down
Loading
Loading