@lamentis/naome 1.3.7 → 1.3.9
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.
- package/Cargo.lock +2 -2
- package/README.md +5 -0
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/install_bridge.rs +56 -8
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/context/select.rs +58 -4
- package/crates/naome-core/src/harness_health/integrity.rs +41 -23
- package/crates/naome-core/src/harness_health/manifest.rs +97 -0
- package/crates/naome-core/src/harness_health.rs +58 -106
- package/crates/naome-core/src/intent/classifier.rs +56 -81
- package/crates/naome-core/src/intent/envelope.rs +173 -19
- package/crates/naome-core/src/intent/legacy_response.rs +2 -0
- package/crates/naome-core/src/intent/model.rs +6 -0
- package/crates/naome-core/src/intent/resolver.rs +25 -0
- package/crates/naome-core/src/intent/risk.rs +11 -1
- package/crates/naome-core/src/intent.rs +1 -1
- package/crates/naome-core/src/quality/cache.rs +122 -19
- package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
- package/crates/naome-core/src/quality/scanner.rs +5 -2
- package/crates/naome-core/src/route/context.rs +8 -0
- package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
- package/crates/naome-core/tests/context.rs +92 -0
- package/crates/naome-core/tests/harness_health.rs +149 -0
- package/crates/naome-core/tests/intent.rs +98 -18
- package/crates/naome-core/tests/intent_support/mod.rs +39 -1
- package/crates/naome-core/tests/intent_v2.rs +299 -10
- package/crates/naome-core/tests/quality_performance.rs +63 -2
- package/crates/naome-core/tests/repo_support/routes.rs +8 -2
- package/crates/naome-core/tests/route_baseline.rs +29 -0
- package/crates/naome-core/tests/route_completion.rs +26 -5
- package/crates/naome-core/tests/route_harness_refresh.rs +7 -1
- package/crates/naome-core/tests/route_user_diff.rs +1 -1
- package/crates/naome-core/tests/task_state_compact.rs +7 -1
- package/installer/filesystem.js +38 -0
- package/installer/flows.js +6 -1
- package/installer/harness-file-ops.js +36 -8
- package/installer/manifest-state.js +2 -2
- package/installer/native.js +63 -18
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +25 -21
- package/templates/naome-root/.naome/bin/check-task-state.js +35 -42
- package/templates/naome-root/.naome/manifest.json +10 -10
- package/templates/naome-root/docs/naome/agent-workflow.md +14 -5
- package/templates/naome-root/docs/naome/architecture.md +9 -0
- package/crates/naome-core/src/intent/patterns.rs +0 -170
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
use std::fs;
|
|
2
|
-
use std::
|
|
1
|
+
use std::fs::{self, OpenOptions};
|
|
2
|
+
use std::io::Write;
|
|
3
|
+
use std::path::{Component, Path, PathBuf};
|
|
3
4
|
|
|
4
5
|
use serde::{Deserialize, Serialize};
|
|
5
6
|
use sha2::{Digest, Sha256};
|
|
@@ -49,7 +50,11 @@ impl QualityCache {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
pub(crate) fn read(&self, path: &str, content_hash: &str) -> Option<FileAnalysis> {
|
|
52
|
-
let
|
|
53
|
+
let cache_path = self.safe_entry_path(path, content_hash).ok()?;
|
|
54
|
+
if !regular_file_without_symlink(&cache_path) {
|
|
55
|
+
return None;
|
|
56
|
+
}
|
|
57
|
+
let entry = fs::read_to_string(cache_path).ok()?;
|
|
53
58
|
let entry: CacheEntry = serde_json::from_str(&entry).ok()?;
|
|
54
59
|
if entry.schema == CACHE_SCHEMA
|
|
55
60
|
&& entry.naome_version == env!("CARGO_PKG_VERSION")
|
|
@@ -70,10 +75,7 @@ impl QualityCache {
|
|
|
70
75
|
content_hash: &str,
|
|
71
76
|
analysis: &FileAnalysis,
|
|
72
77
|
) -> Result<(), NaomeError> {
|
|
73
|
-
let cache_path = self.
|
|
74
|
-
if let Some(parent) = cache_path.parent() {
|
|
75
|
-
fs::create_dir_all(parent)?;
|
|
76
|
-
}
|
|
78
|
+
let cache_path = self.safe_entry_path(path, content_hash)?;
|
|
77
79
|
let entry = CacheEntry {
|
|
78
80
|
schema: CACHE_SCHEMA.to_string(),
|
|
79
81
|
naome_version: env!("CARGO_PKG_VERSION").to_string(),
|
|
@@ -84,31 +86,29 @@ impl QualityCache {
|
|
|
84
86
|
analysis: analysis.clone(),
|
|
85
87
|
};
|
|
86
88
|
let content = format!("{}\n", serde_json::to_string(&entry)?);
|
|
87
|
-
|
|
88
|
-
fs::write(cache_path, content)?;
|
|
89
|
-
}
|
|
90
|
-
Ok(())
|
|
89
|
+
write_cache_entry(&cache_path, &content)
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
fn
|
|
94
|
-
self.root
|
|
92
|
+
fn safe_entry_path(&self, path: &str, content_hash: &str) -> Result<PathBuf, NaomeError> {
|
|
93
|
+
let cache_root = safe_cache_root(&self.root, true)?;
|
|
94
|
+
Ok(cache_root.join(stable_key(&[
|
|
95
95
|
env!("CARGO_PKG_VERSION"),
|
|
96
96
|
&self.config_hash,
|
|
97
97
|
ADAPTER_VERSION,
|
|
98
98
|
path,
|
|
99
99
|
content_hash,
|
|
100
|
-
]))
|
|
100
|
+
])))
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
pub fn quality_cache_status(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
|
|
105
|
-
let path = root
|
|
105
|
+
let path = safe_cache_root(root, false)?;
|
|
106
106
|
let mut entry_count = 0;
|
|
107
107
|
let mut bytes = 0;
|
|
108
|
-
if path
|
|
108
|
+
if regular_directory_without_symlink(&path) {
|
|
109
109
|
for entry in fs::read_dir(&path)? {
|
|
110
110
|
let entry = entry?;
|
|
111
|
-
let metadata = entry.
|
|
111
|
+
let metadata = fs::symlink_metadata(entry.path())?;
|
|
112
112
|
if metadata.is_file() {
|
|
113
113
|
entry_count += 1;
|
|
114
114
|
bytes += metadata.len();
|
|
@@ -124,13 +124,116 @@ pub fn quality_cache_status(root: &Path) -> Result<QualityCacheStatus, NaomeErro
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
pub fn clear_quality_cache(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
|
|
127
|
-
let path = root
|
|
128
|
-
if path
|
|
127
|
+
let path = safe_cache_root(root, false)?;
|
|
128
|
+
if regular_directory_without_symlink(&path) {
|
|
129
129
|
fs::remove_dir_all(&path)?;
|
|
130
130
|
}
|
|
131
131
|
quality_cache_status(root)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
fn write_cache_entry(cache_path: &Path, content: &str) -> Result<(), NaomeError> {
|
|
135
|
+
validate_cache_entry_parent(cache_path)?;
|
|
136
|
+
match fs::symlink_metadata(cache_path) {
|
|
137
|
+
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
|
|
138
|
+
return Err(NaomeError::new(format!(
|
|
139
|
+
"quality cache entry must be a regular file: {}",
|
|
140
|
+
cache_path.display()
|
|
141
|
+
)));
|
|
142
|
+
}
|
|
143
|
+
Ok(_) => {
|
|
144
|
+
if fs::read_to_string(cache_path).map_or(false, |current| current == content) {
|
|
145
|
+
return Ok(());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
|
149
|
+
Err(error) => return Err(error.into()),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let temp_path = cache_path.with_extension(format!(
|
|
153
|
+
"tmp.{}.{}",
|
|
154
|
+
std::process::id(),
|
|
155
|
+
stable_key(&[content]).trim_end_matches(".json")
|
|
156
|
+
));
|
|
157
|
+
validate_cache_entry_parent(&temp_path)?;
|
|
158
|
+
let mut temp = OpenOptions::new()
|
|
159
|
+
.write(true)
|
|
160
|
+
.create_new(true)
|
|
161
|
+
.open(&temp_path)?;
|
|
162
|
+
if let Err(error) = temp.write_all(content.as_bytes()) {
|
|
163
|
+
let _ = fs::remove_file(&temp_path);
|
|
164
|
+
return Err(error.into());
|
|
165
|
+
}
|
|
166
|
+
if let Err(error) = temp.sync_all() {
|
|
167
|
+
let _ = fs::remove_file(&temp_path);
|
|
168
|
+
return Err(error.into());
|
|
169
|
+
}
|
|
170
|
+
drop(temp);
|
|
171
|
+
validate_cache_entry_parent(cache_path)?;
|
|
172
|
+
if let Err(error) = fs::rename(&temp_path, cache_path) {
|
|
173
|
+
let _ = fs::remove_file(&temp_path);
|
|
174
|
+
return Err(error.into());
|
|
175
|
+
}
|
|
176
|
+
Ok(())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn validate_cache_entry_parent(cache_path: &Path) -> Result<(), NaomeError> {
|
|
180
|
+
let Some(parent) = cache_path.parent() else {
|
|
181
|
+
return Err(NaomeError::new("quality cache entry path has no parent"));
|
|
182
|
+
};
|
|
183
|
+
if !regular_directory_without_symlink(parent) {
|
|
184
|
+
return Err(NaomeError::new(format!(
|
|
185
|
+
"quality cache path must be a regular directory without symlinks: {}",
|
|
186
|
+
parent.display()
|
|
187
|
+
)));
|
|
188
|
+
}
|
|
189
|
+
Ok(())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn safe_cache_root(root: &Path, create: bool) -> Result<PathBuf, NaomeError> {
|
|
193
|
+
let mut current = root.to_path_buf();
|
|
194
|
+
for component in Path::new(CACHE_RELATIVE_PATH).components() {
|
|
195
|
+
let Component::Normal(component) = component else {
|
|
196
|
+
return Err(NaomeError::new(
|
|
197
|
+
"quality cache path must be repository-relative",
|
|
198
|
+
));
|
|
199
|
+
};
|
|
200
|
+
current.push(component);
|
|
201
|
+
match fs::symlink_metadata(¤t) {
|
|
202
|
+
Ok(metadata) if metadata.file_type().is_symlink() => {
|
|
203
|
+
return Err(NaomeError::new(format!(
|
|
204
|
+
"quality cache path must not contain symlinks: {}",
|
|
205
|
+
current.display()
|
|
206
|
+
)));
|
|
207
|
+
}
|
|
208
|
+
Ok(metadata) if metadata.is_dir() => {}
|
|
209
|
+
Ok(_) => {
|
|
210
|
+
return Err(NaomeError::new(format!(
|
|
211
|
+
"quality cache path component is not a directory: {}",
|
|
212
|
+
current.display()
|
|
213
|
+
)));
|
|
214
|
+
}
|
|
215
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound && create => {
|
|
216
|
+
fs::create_dir(¤t)?;
|
|
217
|
+
}
|
|
218
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(current),
|
|
219
|
+
Err(error) => return Err(error.into()),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
Ok(current)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn regular_directory_without_symlink(path: &Path) -> bool {
|
|
226
|
+
fs::symlink_metadata(path)
|
|
227
|
+
.map(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink())
|
|
228
|
+
.unwrap_or(false)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn regular_file_without_symlink(path: &Path) -> bool {
|
|
232
|
+
fs::symlink_metadata(path)
|
|
233
|
+
.map(|metadata| metadata.is_file() && !metadata.file_type().is_symlink())
|
|
234
|
+
.unwrap_or(false)
|
|
235
|
+
}
|
|
236
|
+
|
|
134
237
|
pub(crate) fn content_hash(content: &str) -> String {
|
|
135
238
|
stable_key(&[content])
|
|
136
239
|
}
|
|
@@ -8,6 +8,8 @@ use super::{FileAnalysis, NormalizedLine, SymbolAnalysis};
|
|
|
8
8
|
use crate::quality::cache::{content_hash, QualityCache};
|
|
9
9
|
use normalize::{normalize_line, token_set};
|
|
10
10
|
|
|
11
|
+
use super::repo_paths::regular_repo_file_path;
|
|
12
|
+
|
|
11
13
|
pub(super) fn analyze_repo_file(
|
|
12
14
|
root: &Path,
|
|
13
15
|
path: &str,
|
|
@@ -16,8 +18,8 @@ pub(super) fn analyze_repo_file(
|
|
|
16
18
|
cache: &QualityCache,
|
|
17
19
|
allow_cache: bool,
|
|
18
20
|
) -> Option<(FileAnalysis, bool)> {
|
|
19
|
-
let full_path = root
|
|
20
|
-
if
|
|
21
|
+
let full_path = regular_repo_file_path(root, path)?;
|
|
22
|
+
if is_binary_extension(path) {
|
|
21
23
|
return None;
|
|
22
24
|
}
|
|
23
25
|
if allow_cache {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use std::collections::{HashMap, HashSet};
|
|
2
2
|
use std::fs;
|
|
3
|
-
use std::path::Path;
|
|
3
|
+
use std::path::{Component, Path};
|
|
4
4
|
use std::process::Command;
|
|
5
5
|
|
|
6
6
|
use crate::models::NaomeError;
|
|
@@ -44,8 +44,10 @@ pub(super) fn added_lines_by_path(
|
|
|
44
44
|
if !target_paths.contains(&path) {
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
|
-
if let
|
|
48
|
-
|
|
47
|
+
if let Some(file_path) = regular_repo_file_path(root, &path) {
|
|
48
|
+
if let Ok(content) = fs::read_to_string(file_path) {
|
|
49
|
+
added.insert(path, content.lines().count());
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
Ok(added)
|
|
@@ -92,3 +94,25 @@ fn untracked_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
|
92
94
|
.map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
|
|
93
95
|
.collect())
|
|
94
96
|
}
|
|
97
|
+
|
|
98
|
+
pub(super) fn regular_repo_file_path(root: &Path, path: &str) -> Option<std::path::PathBuf> {
|
|
99
|
+
let relative = Path::new(path);
|
|
100
|
+
if relative.is_absolute() {
|
|
101
|
+
return None;
|
|
102
|
+
}
|
|
103
|
+
let mut current = root.to_path_buf();
|
|
104
|
+
for component in relative.components() {
|
|
105
|
+
let Component::Normal(component) = component else {
|
|
106
|
+
return None;
|
|
107
|
+
};
|
|
108
|
+
current.push(component);
|
|
109
|
+
let metadata = fs::symlink_metadata(¤t).ok()?;
|
|
110
|
+
if metadata.file_type().is_symlink() {
|
|
111
|
+
return None;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
fs::symlink_metadata(¤t)
|
|
115
|
+
.ok()
|
|
116
|
+
.filter(|metadata| metadata.is_file())
|
|
117
|
+
.map(|_| current)
|
|
118
|
+
}
|
|
@@ -9,7 +9,7 @@ use sha2::{Digest, Sha256};
|
|
|
9
9
|
|
|
10
10
|
use crate::{git, models::NaomeError, paths};
|
|
11
11
|
pub(crate) use repo_paths::collect_repo_paths;
|
|
12
|
-
use repo_paths::{added_lines_by_path, tracked_blob_hashes};
|
|
12
|
+
use repo_paths::{added_lines_by_path, regular_repo_file_path, tracked_blob_hashes};
|
|
13
13
|
|
|
14
14
|
use super::cache::QualityCache;
|
|
15
15
|
use super::types::{
|
|
@@ -317,7 +317,10 @@ fn max_file_bytes(mode: QualityMode) -> u64 {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
fn file_exceeds_budget(root: &Path, path: &str, mode: QualityMode) -> bool {
|
|
320
|
-
|
|
320
|
+
let Some(full_path) = regular_repo_file_path(root, path) else {
|
|
321
|
+
return false;
|
|
322
|
+
};
|
|
323
|
+
fs::symlink_metadata(full_path).map_or(false, |metadata| {
|
|
321
324
|
metadata.is_file() && metadata.len() > max_file_bytes(mode)
|
|
322
325
|
})
|
|
323
326
|
}
|
|
@@ -57,6 +57,8 @@ pub(super) fn winning_rule(intent: &IntentDecision) -> String {
|
|
|
57
57
|
"current_task_revision_continues_task"
|
|
58
58
|
}
|
|
59
59
|
"answer_status_only" => "status_request_read_only",
|
|
60
|
+
"normalize_prompt_first" => "prompt_envelope_required_before_routing",
|
|
61
|
+
"answer_without_mutation" => "advisory_prompt_read_only",
|
|
60
62
|
"create_new_task" | "create_new_task_without_auto_baseline" => "ready_repo_new_task",
|
|
61
63
|
"create_isolated_task_worktree" => "dirty_repo_new_task_worktree_isolation",
|
|
62
64
|
"commit_user_diff_with_quality_gate" => "explicit_user_diff_commit_quality_gate",
|
|
@@ -164,6 +166,12 @@ pub(super) fn required_context_for_intent(intent: &IntentDecision) -> Vec<String
|
|
|
164
166
|
"answer_status_only" => {
|
|
165
167
|
push_unique(&mut context, "docs/naome/index.md");
|
|
166
168
|
}
|
|
169
|
+
"normalize_prompt_first" => {
|
|
170
|
+
push_unique(&mut context, "docs/naome/agent-workflow.md");
|
|
171
|
+
}
|
|
172
|
+
"answer_without_mutation" => {
|
|
173
|
+
push_unique(&mut context, "docs/naome/index.md");
|
|
174
|
+
}
|
|
167
175
|
"repair_harness_only" => {
|
|
168
176
|
push_unique(&mut context, ".naome/manifest.json");
|
|
169
177
|
push_unique(&mut context, "docs/naome/index.md");
|
|
@@ -23,8 +23,14 @@ pub(super) fn refresh_support_files(
|
|
|
23
23
|
changed.push(path.to_string());
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
for path in [
|
|
27
|
+
NAOME_COMMAND_PATH,
|
|
28
|
+
HEALTH_CHECKER_PATH,
|
|
29
|
+
TASK_STATE_CHECKER_PATH,
|
|
30
|
+
] {
|
|
31
|
+
if replace_native_integrity(root, path, integrity)? {
|
|
32
|
+
changed.push(path.to_string());
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
Ok(changed)
|
|
30
36
|
}
|
|
@@ -76,12 +82,13 @@ fn render_expected_integrity_block(integrity: &BTreeMap<String, String>) -> Stri
|
|
|
76
82
|
|
|
77
83
|
fn replace_native_integrity(
|
|
78
84
|
root: &Path,
|
|
85
|
+
relative_path: &str,
|
|
79
86
|
integrity: &BTreeMap<String, String>,
|
|
80
87
|
) -> Result<bool, NaomeError> {
|
|
81
88
|
let Some(native_hash) = integrity.get(NATIVE_BINARY_PATH) else {
|
|
82
89
|
return Ok(false);
|
|
83
90
|
};
|
|
84
|
-
let path = root.join(
|
|
91
|
+
let path = root.join(relative_path);
|
|
85
92
|
if !path.is_file() {
|
|
86
93
|
return Ok(false);
|
|
87
94
|
}
|
|
@@ -66,6 +66,91 @@ fn prompt_context_uses_file_mentions_without_broad_markdown_context() {
|
|
|
66
66
|
.any(|item| item.path == "docs/naome/repository-quality.md"));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
#[test]
|
|
70
|
+
fn prompt_context_ignores_nonexistent_concept_tokens_that_look_path_like() {
|
|
71
|
+
let repo = context_repo("context-prompt-concept-terms");
|
|
72
|
+
repo.write_file("docs/naome/repository-quality.md", "# Quality\n");
|
|
73
|
+
repo.commit_all("baseline");
|
|
74
|
+
|
|
75
|
+
let selection = select_context_for_prompt(
|
|
76
|
+
repo.path(),
|
|
77
|
+
"Advisory/planning-only prompts and German/English examples must not become paths.",
|
|
78
|
+
)
|
|
79
|
+
.unwrap();
|
|
80
|
+
|
|
81
|
+
assert_eq!(selection.mode, "prompt");
|
|
82
|
+
assert_eq!(
|
|
83
|
+
selection.required_context[0].path,
|
|
84
|
+
".naome/verification.json"
|
|
85
|
+
);
|
|
86
|
+
assert!(!selection
|
|
87
|
+
.required_context
|
|
88
|
+
.iter()
|
|
89
|
+
.any(|item| item.path == "Advisory/planning-only" || item.path == "German/English"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[test]
|
|
93
|
+
fn prompt_context_prefers_envelope_referenced_paths_over_raw_prompt_tokens() {
|
|
94
|
+
let repo = context_repo("context-prompt-envelope-paths");
|
|
95
|
+
repo.write_file("packages/app/src/lib.rs", "pub fn app() {}\n");
|
|
96
|
+
repo.write_file("packages/app/src/other.rs", "pub fn other() {}\n");
|
|
97
|
+
repo.commit_all("baseline");
|
|
98
|
+
|
|
99
|
+
let selection = select_context_for_prompt(
|
|
100
|
+
repo.path(),
|
|
101
|
+
"```naome-prompt-envelope-v1\n{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[\"packages/app/src/lib.rs\"],\"constraints\":[],\"uncertainties\":[]}\n```\n\nPlease mention packages/app/src/other.rs in prose but use the envelope path.",
|
|
102
|
+
)
|
|
103
|
+
.unwrap();
|
|
104
|
+
|
|
105
|
+
assert_eq!(selection.mode, "prompt");
|
|
106
|
+
assert_eq!(
|
|
107
|
+
selection.required_context[0].path,
|
|
108
|
+
"packages/app/src/lib.rs"
|
|
109
|
+
);
|
|
110
|
+
assert!(!selection
|
|
111
|
+
.required_context
|
|
112
|
+
.iter()
|
|
113
|
+
.any(|item| item.path == "packages/app/src/other.rs"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn prompt_context_keeps_envelope_paths_for_nested_creation_targets() {
|
|
118
|
+
let repo = app_context_repo("context-prompt-envelope-new-paths");
|
|
119
|
+
|
|
120
|
+
let selection = select_context_for_prompt(
|
|
121
|
+
repo.path(),
|
|
122
|
+
"```naome-prompt-envelope-v1\n{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"create_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[\"packages/app/src/new/mod.rs\"],\"constraints\":[],\"uncertainties\":[]}\n```\n\nCreate the new module.",
|
|
123
|
+
)
|
|
124
|
+
.unwrap();
|
|
125
|
+
|
|
126
|
+
assert_eq!(selection.mode, "prompt");
|
|
127
|
+
assert_eq!(
|
|
128
|
+
selection.required_context[0].path,
|
|
129
|
+
"packages/app/src/new/mod.rs"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn prompt_context_rejects_envelope_paths_outside_repository() {
|
|
135
|
+
let repo = app_context_repo("context-prompt-envelope-safe-paths");
|
|
136
|
+
|
|
137
|
+
let selection = select_context_for_prompt(
|
|
138
|
+
repo.path(),
|
|
139
|
+
"```naome-prompt-envelope-v1\n{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[\"../notes.md\",\"/tmp/file.rs\",\"packages/app/src/lib.rs\"],\"constraints\":[],\"uncertainties\":[]}\n```",
|
|
140
|
+
)
|
|
141
|
+
.unwrap();
|
|
142
|
+
|
|
143
|
+
assert_eq!(selection.mode, "prompt");
|
|
144
|
+
assert_eq!(
|
|
145
|
+
selection.required_context[0].path,
|
|
146
|
+
"packages/app/src/lib.rs"
|
|
147
|
+
);
|
|
148
|
+
assert!(!selection
|
|
149
|
+
.required_context
|
|
150
|
+
.iter()
|
|
151
|
+
.any(|item| item.path == "../notes.md" || item.path == "/tmp/file.rs"));
|
|
152
|
+
}
|
|
153
|
+
|
|
69
154
|
#[test]
|
|
70
155
|
fn context_selection_reports_over_budget_when_many_paths_change() {
|
|
71
156
|
let repo = context_repo("context-budget");
|
|
@@ -97,3 +182,10 @@ fn context_repo(name: &str) -> TestRepo {
|
|
|
97
182
|
);
|
|
98
183
|
repo
|
|
99
184
|
}
|
|
185
|
+
|
|
186
|
+
fn app_context_repo(name: &str) -> TestRepo {
|
|
187
|
+
let repo = context_repo(name);
|
|
188
|
+
repo.write_file("packages/app/src/lib.rs", "pub fn app() {}\n");
|
|
189
|
+
repo.commit_all("baseline");
|
|
190
|
+
repo
|
|
191
|
+
}
|
|
@@ -24,6 +24,11 @@ const MACHINE_OWNED_PATHS: &[&str] = &[
|
|
|
24
24
|
"docs/naome/upgrade.md",
|
|
25
25
|
];
|
|
26
26
|
|
|
27
|
+
#[cfg(windows)]
|
|
28
|
+
const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust.exe";
|
|
29
|
+
#[cfg(not(windows))]
|
|
30
|
+
const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust";
|
|
31
|
+
|
|
27
32
|
const PROJECT_OWNED_PATHS: &[&str] = &[
|
|
28
33
|
".naomeignore",
|
|
29
34
|
".naome/init-state.json",
|
|
@@ -78,6 +83,99 @@ fn rejects_drifted_machine_owned_files() {
|
|
|
78
83
|
assert!(joined.contains("docs/naome/execution.md"));
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
#[test]
|
|
87
|
+
fn accepts_native_decision_binary_with_manifest_ownership_and_integrity() {
|
|
88
|
+
let mut repo = HarnessFixture::new();
|
|
89
|
+
repo.install_native_decision_binary("native decision fixture\n");
|
|
90
|
+
|
|
91
|
+
let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
|
|
92
|
+
|
|
93
|
+
assert!(errors.is_empty(), "{errors:#?}");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn rejects_native_decision_binary_when_manifest_ownership_is_removed() {
|
|
98
|
+
let mut repo = HarnessFixture::new();
|
|
99
|
+
repo.install_native_decision_binary("native decision fixture\n");
|
|
100
|
+
repo.remove_native_manifest_entry();
|
|
101
|
+
|
|
102
|
+
let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
|
|
103
|
+
let joined = errors.join("\n");
|
|
104
|
+
|
|
105
|
+
assert!(
|
|
106
|
+
joined.contains(&format!(
|
|
107
|
+
".naome/manifest.json machineOwned must include {NATIVE_BINARY_PATH}."
|
|
108
|
+
)),
|
|
109
|
+
"{joined}"
|
|
110
|
+
);
|
|
111
|
+
assert!(
|
|
112
|
+
joined.contains(&format!(
|
|
113
|
+
".naome/manifest.json integrity missing {NATIVE_BINARY_PATH}."
|
|
114
|
+
)),
|
|
115
|
+
"{joined}"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn rejects_checker_declared_native_integrity_without_native_manifest_entry() {
|
|
121
|
+
let mut repo = HarnessFixture::new();
|
|
122
|
+
let checker_path = ".naome/bin/check-task-state.js";
|
|
123
|
+
let native_hash = format!("sha256:{}", "a".repeat(64));
|
|
124
|
+
repo.write(
|
|
125
|
+
checker_path,
|
|
126
|
+
&format!("const expectedNativeBinaryIntegrity = \"{native_hash}\";\n"),
|
|
127
|
+
);
|
|
128
|
+
repo.integrity.insert(
|
|
129
|
+
checker_path.to_string(),
|
|
130
|
+
format!(
|
|
131
|
+
"sha256:{}",
|
|
132
|
+
sha256("const expectedNativeBinaryIntegrity = \"sha256:generated\";\n")
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
write_file(
|
|
136
|
+
repo.path(),
|
|
137
|
+
".naome/manifest.json",
|
|
138
|
+
&pretty_json(manifest_fixture(&repo.integrity)),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
|
|
142
|
+
let joined = errors.join("\n");
|
|
143
|
+
|
|
144
|
+
assert!(
|
|
145
|
+
joined.contains(&format!(
|
|
146
|
+
".naome/manifest.json machineOwned must include {NATIVE_BINARY_PATH}."
|
|
147
|
+
)),
|
|
148
|
+
"{joined}"
|
|
149
|
+
);
|
|
150
|
+
assert!(
|
|
151
|
+
joined.contains(&format!("{NATIVE_BINARY_PATH} is missing.")),
|
|
152
|
+
"{joined}"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn rejects_native_integrity_assignment_with_appended_code() {
|
|
158
|
+
let mut repo = HarnessFixture::new();
|
|
159
|
+
repo.install_native_decision_binary("native decision fixture\n");
|
|
160
|
+
let native_hash = repo.integrity.get(NATIVE_BINARY_PATH).unwrap().clone();
|
|
161
|
+
repo.write(
|
|
162
|
+
".naome/bin/check-harness-health.js",
|
|
163
|
+
&format!("const expectedNativeBinaryIntegrity = \"{native_hash}\"; process.exit(0);\n"),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
|
|
167
|
+
let joined = errors.join("\n");
|
|
168
|
+
|
|
169
|
+
assert!(
|
|
170
|
+
joined.contains(".naome/bin/check-harness-health.js integrity mismatch"),
|
|
171
|
+
"{joined}"
|
|
172
|
+
);
|
|
173
|
+
assert!(
|
|
174
|
+
joined.contains(".naome/bin/check-harness-health.js native binary integrity does not match .naome/manifest.json."),
|
|
175
|
+
"{joined}"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
81
179
|
#[test]
|
|
82
180
|
fn rejects_missing_archive_ignore_boundary() {
|
|
83
181
|
let repo = HarnessFixture::new();
|
|
@@ -175,6 +273,53 @@ impl HarnessFixture {
|
|
|
175
273
|
fn write(&self, relative_path: &str, content: &str) {
|
|
176
274
|
write_file(&self.root, relative_path, content);
|
|
177
275
|
}
|
|
276
|
+
|
|
277
|
+
fn install_native_decision_binary(&mut self, content: &str) {
|
|
278
|
+
let native_hash = format!("sha256:{}", sha256(content));
|
|
279
|
+
write_file(&self.root, NATIVE_BINARY_PATH, content);
|
|
280
|
+
self.integrity
|
|
281
|
+
.insert(NATIVE_BINARY_PATH.to_string(), native_hash.clone());
|
|
282
|
+
|
|
283
|
+
for relative_path in [
|
|
284
|
+
".naome/bin/naome.js",
|
|
285
|
+
".naome/bin/check-harness-health.js",
|
|
286
|
+
".naome/bin/check-task-state.js",
|
|
287
|
+
] {
|
|
288
|
+
let command_content =
|
|
289
|
+
format!("const expectedNativeBinaryIntegrity = \"{native_hash}\";\n");
|
|
290
|
+
let normalized_command_content =
|
|
291
|
+
"const expectedNativeBinaryIntegrity = \"sha256:generated\";\n";
|
|
292
|
+
write_file(&self.root, relative_path, &command_content);
|
|
293
|
+
self.integrity.insert(
|
|
294
|
+
relative_path.to_string(),
|
|
295
|
+
format!("sha256:{}", sha256(normalized_command_content)),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
self.write_manifest_with_native_entry();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fn remove_native_manifest_entry(&self) {
|
|
303
|
+
let mut manifest = read_json_value(&self.root, ".naome/manifest.json");
|
|
304
|
+
manifest["machineOwned"]
|
|
305
|
+
.as_array_mut()
|
|
306
|
+
.unwrap()
|
|
307
|
+
.retain(|entry| entry.as_str() != Some(NATIVE_BINARY_PATH));
|
|
308
|
+
manifest["integrity"]
|
|
309
|
+
.as_object_mut()
|
|
310
|
+
.unwrap()
|
|
311
|
+
.remove(NATIVE_BINARY_PATH);
|
|
312
|
+
write_file(&self.root, ".naome/manifest.json", &pretty_json(manifest));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fn write_manifest_with_native_entry(&self) {
|
|
316
|
+
let mut manifest = manifest_fixture(&self.integrity);
|
|
317
|
+
manifest["machineOwned"]
|
|
318
|
+
.as_array_mut()
|
|
319
|
+
.unwrap()
|
|
320
|
+
.push(json!(NATIVE_BINARY_PATH));
|
|
321
|
+
write_file(&self.root, ".naome/manifest.json", &pretty_json(manifest));
|
|
322
|
+
}
|
|
178
323
|
}
|
|
179
324
|
|
|
180
325
|
fn manifest_fixture(integrity: &HashMap<String, String>) -> serde_json::Value {
|
|
@@ -217,6 +362,10 @@ fn machine_content(relative_path: &str) -> String {
|
|
|
217
362
|
}
|
|
218
363
|
}
|
|
219
364
|
|
|
365
|
+
fn read_json_value(root: &Path, relative_path: &str) -> serde_json::Value {
|
|
366
|
+
serde_json::from_str(&fs::read_to_string(root.join(relative_path)).unwrap()).unwrap()
|
|
367
|
+
}
|
|
368
|
+
|
|
220
369
|
fn write_file(root: &Path, relative_path: &str, content: &str) {
|
|
221
370
|
let path = root.join(relative_path);
|
|
222
371
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|