buildwithnexus 0.10.4 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -0
  2. package/harness/Cargo.lock +6 -5
  3. package/harness/Cargo.toml +2 -8
  4. package/harness/src/agent.rs +2665 -246
  5. package/harness/src/bundled_skills/code-review.md +17 -0
  6. package/harness/src/bundled_skills/codebase-repair.md +32 -0
  7. package/harness/src/bundled_skills/data-analysis.md +16 -0
  8. package/harness/src/bundled_skills/decision_support.md +59 -0
  9. package/harness/src/bundled_skills/document-generation.md +29 -0
  10. package/harness/src/bundled_skills/frontend-ux.md +18 -0
  11. package/harness/src/bundled_skills/git-release.md +23 -0
  12. package/harness/src/bundled_skills/release-notes.md +17 -0
  13. package/harness/src/bundled_skills/research.md +17 -0
  14. package/harness/src/bundled_skills/rust-cli.md +26 -0
  15. package/harness/src/bundled_skills/security-review.md +19 -0
  16. package/harness/src/bundled_skills/self-knowledge.md +104 -0
  17. package/harness/src/bundled_skills/spec-writing.md +19 -0
  18. package/harness/src/bundled_skills/static-app.md +36 -0
  19. package/harness/src/bundled_skills/test-engineering.md +18 -0
  20. package/harness/src/bundled_skills/tool-use.md +52 -0
  21. package/harness/src/bundled_skills/verification_workflow.md +62 -0
  22. package/harness/src/checkpoint.rs +105 -8
  23. package/harness/src/config.rs +533 -88
  24. package/harness/src/hooks.rs +306 -52
  25. package/harness/src/knowledge.rs +433 -0
  26. package/harness/src/lib.rs +2127 -249
  27. package/harness/src/local.rs +13 -4
  28. package/harness/src/onboarding.rs +91 -21
  29. package/harness/src/provider.rs +346 -79
  30. package/harness/src/report.rs +37 -13
  31. package/harness/src/rules.rs +467 -0
  32. package/harness/src/session.rs +36 -9
  33. package/harness/src/tools.rs +3021 -114
  34. package/harness/src/trace.rs +218 -0
  35. package/harness/src/tui.rs +2174 -237
  36. package/harness/src/verifier.rs +375 -0
  37. package/harness/src/workflow.rs +72 -32
  38. package/package.json +1 -1
  39. package/scripts/postinstall.js +4 -1
  40. package/scripts/resolve-binary.js +2 -0
@@ -9,6 +9,7 @@ use crate::config;
9
9
 
10
10
  const MAX_SNAPSHOT_BYTES: u64 = 2 * 1024 * 1024;
11
11
 
12
+ /// Represents a file modification snapshot recorded prior to an edit tool operation.
12
13
  #[derive(Clone, Debug, Serialize, Deserialize)]
13
14
  pub struct Checkpoint {
14
15
  pub id: String,
@@ -32,7 +33,8 @@ fn dir(cwd: &Path) -> PathBuf {
32
33
  .join(sanitize_id_part(&cwd.to_string_lossy()))
33
34
  }
34
35
 
35
- fn now_ms() -> u128 {
36
+ /// Returns the current Unix timestamp in milliseconds.
37
+ pub fn now_ms() -> u128 {
36
38
  SystemTime::now()
37
39
  .duration_since(UNIX_EPOCH)
38
40
  .map(|d| d.as_millis())
@@ -45,6 +47,8 @@ fn sanitize_id_part(s: &str) -> String {
45
47
  .collect()
46
48
  }
47
49
 
50
+ /// Records a file modification checkpoint before an edit tool mutates `path`.
51
+ /// Stores the previous contents (or empty if the file did not exist) in `.buildwithnexus/checkpoints`.
48
52
  pub fn record(cwd: &Path, path: &Path, action: &str) {
49
53
  let existed = path.exists();
50
54
  let content = if existed {
@@ -73,6 +77,7 @@ pub fn record(cwd: &Path, path: &Path, action: &str) {
73
77
  }
74
78
  }
75
79
 
80
+ /// Returns all recorded checkpoints for the given workspace directory, sorted newest first.
76
81
  pub fn list(cwd: &Path) -> Vec<Checkpoint> {
77
82
  let Ok(rd) = fs::read_dir(dir(cwd)) else {
78
83
  return Vec::new();
@@ -87,18 +92,110 @@ pub fn list(cwd: &Path) -> Vec<Checkpoint> {
87
92
  items
88
93
  }
89
94
 
90
- pub fn undo_latest(cwd: &Path) -> Result<Checkpoint, String> {
91
- let Some(cp) = list(cwd).into_iter().next() else {
92
- return Err("no checkpoints for this directory".into());
93
- };
95
+ fn restore_one(cp: &Checkpoint) -> Result<(), String> {
94
96
  if cp.existed {
95
97
  if let Some(parent) = cp.path.parent() {
96
- fs::create_dir_all(parent).map_err(|e| format!("cannot create {}: {e}", parent.display()))?;
98
+ fs::create_dir_all(parent)
99
+ .map_err(|e| format!("cannot create {}: {e}", parent.display()))?;
97
100
  }
98
- fs::write(&cp.path, &cp.content).map_err(|e| format!("cannot restore {}: {e}", cp.path.display()))?;
101
+ fs::write(&cp.path, &cp.content)
102
+ .map_err(|e| format!("cannot restore {}: {e}", cp.path.display()))?;
99
103
  } else if cp.path.exists() {
100
- fs::remove_file(&cp.path).map_err(|e| format!("cannot remove {}: {e}", cp.path.display()))?;
104
+ fs::remove_file(&cp.path)
105
+ .map_err(|e| format!("cannot remove {}: {e}", cp.path.display()))?;
101
106
  }
107
+ Ok(())
108
+ }
109
+
110
+ /// Restores the most recently recorded checkpoint for the workspace, removing its checkpoint file.
111
+ pub fn undo_latest(cwd: &Path) -> Result<Checkpoint, String> {
112
+ let Some(cp) = list(cwd).into_iter().next() else {
113
+ return Err("no checkpoints for this directory".into());
114
+ };
115
+ restore_one(&cp)?;
102
116
  let _ = fs::remove_file(dir(cwd).join(format!("{}.json", cp.id)));
103
117
  Ok(cp)
104
118
  }
119
+
120
+ /// Restores a specific checkpoint by its unique ID (`<timestamp>-<action>`), removing its checkpoint file.
121
+ pub fn undo_by_id(cwd: &Path, id: &str) -> Result<Checkpoint, String> {
122
+ let all = list(cwd);
123
+ let Some(cp) = all.into_iter().find(|c| c.id == id) else {
124
+ return Err(format!("checkpoint id not found: {id}"));
125
+ };
126
+ restore_one(&cp)?;
127
+ let _ = fs::remove_file(dir(cwd).join(format!("{}.json", cp.id)));
128
+ Ok(cp)
129
+ }
130
+
131
+ /// Restores all checkpoints recorded at or after `since_ms`, rolling back multiple edits in reverse chronological order.
132
+ pub fn undo_all_since(cwd: &Path, since_ms: u128) -> Result<Vec<Checkpoint>, String> {
133
+ let all = list(cwd);
134
+ let mut restored = Vec::new();
135
+ for cp in all {
136
+ if cp.created_ms >= since_ms {
137
+ restore_one(&cp)?;
138
+ let _ = fs::remove_file(dir(cwd).join(format!("{}.json", cp.id)));
139
+ restored.push(cp);
140
+ }
141
+ }
142
+ if restored.is_empty() {
143
+ Err("no checkpoints found in that timeframe".into())
144
+ } else {
145
+ Ok(restored)
146
+ }
147
+ }
148
+
149
+ /// Performs a hard rollback of the workspace using `git checkout -- .`, discarding all unstaged working directory changes.
150
+ pub fn git_rollback(cwd: &Path) -> Result<String, String> {
151
+ let mut out = String::new();
152
+ let st = std::process::Command::new("git")
153
+ .args(["checkout", "--", "."])
154
+ .current_dir(cwd)
155
+ .output();
156
+ if let Ok(o) = st {
157
+ out.push_str(&String::from_utf8_lossy(&o.stdout));
158
+ out.push_str(&String::from_utf8_lossy(&o.stderr));
159
+ } else {
160
+ return Err("git checkout failed".into());
161
+ }
162
+ let st2 = std::process::Command::new("git")
163
+ .args(["clean", "-fd"])
164
+ .current_dir(cwd)
165
+ .output();
166
+ if let Ok(o) = st2 {
167
+ out.push_str(&String::from_utf8_lossy(&o.stdout));
168
+ }
169
+ Ok(if out.trim().is_empty() {
170
+ "working tree reset cleanly".to_string()
171
+ } else {
172
+ out.trim().to_string()
173
+ })
174
+ }
175
+
176
+ #[cfg(test)]
177
+ mod tests {
178
+ use super::*;
179
+ use std::fs;
180
+
181
+ #[test]
182
+ fn test_record_and_undo_by_id() {
183
+ let d = std::env::temp_dir().join(format!("bwn-cp-test-{}", std::process::id()));
184
+ let _ = fs::remove_dir_all(&d);
185
+ fs::create_dir_all(&d).unwrap();
186
+ let file_path = d.join("test.txt");
187
+ fs::write(&file_path, "initial content").unwrap();
188
+
189
+ record(&d, &file_path, "edit_file");
190
+ fs::write(&file_path, "modified content").unwrap();
191
+
192
+ let cps = list(&d);
193
+ assert!(!cps.is_empty());
194
+ let cp_id = &cps[0].id;
195
+
196
+ let res = undo_by_id(&d, cp_id);
197
+ assert!(res.is_ok());
198
+ assert_eq!(fs::read_to_string(&file_path).unwrap(), "initial content");
199
+ let _ = fs::remove_dir_all(&d);
200
+ }
201
+ }