@lamentis/naome 1.0.0

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 (49) hide show
  1. package/Cargo.lock +199 -0
  2. package/Cargo.toml +11 -0
  3. package/LICENSE +21 -0
  4. package/README.md +6 -0
  5. package/bin/naome-node.js +1424 -0
  6. package/bin/naome.js +129 -0
  7. package/crates/naome-cli/Cargo.toml +14 -0
  8. package/crates/naome-cli/src/main.rs +341 -0
  9. package/crates/naome-core/Cargo.toml +11 -0
  10. package/crates/naome-core/src/decision.rs +432 -0
  11. package/crates/naome-core/src/git.rs +70 -0
  12. package/crates/naome-core/src/harness_health.rs +557 -0
  13. package/crates/naome-core/src/install_plan.rs +82 -0
  14. package/crates/naome-core/src/lib.rs +17 -0
  15. package/crates/naome-core/src/models.rs +99 -0
  16. package/crates/naome-core/src/paths.rs +72 -0
  17. package/crates/naome-core/src/task_state.rs +1859 -0
  18. package/crates/naome-core/src/verification.rs +217 -0
  19. package/crates/naome-core/src/verification_contract.rs +406 -0
  20. package/crates/naome-core/tests/decision.rs +297 -0
  21. package/crates/naome-core/tests/harness_health.rs +232 -0
  22. package/crates/naome-core/tests/install_plan.rs +35 -0
  23. package/crates/naome-core/tests/task_state.rs +588 -0
  24. package/crates/naome-core/tests/verification.rs +165 -0
  25. package/crates/naome-core/tests/verification_contract.rs +181 -0
  26. package/native/darwin-arm64/naome +0 -0
  27. package/package.json +44 -0
  28. package/templates/naome-root/.naome/bin/check-harness-health.js +163 -0
  29. package/templates/naome-root/.naome/bin/check-task-state.js +180 -0
  30. package/templates/naome-root/.naome/bin/naome.js +306 -0
  31. package/templates/naome-root/.naome/init-state.json +13 -0
  32. package/templates/naome-root/.naome/manifest.json +45 -0
  33. package/templates/naome-root/.naome/package.json +3 -0
  34. package/templates/naome-root/.naome/task-contract.schema.json +174 -0
  35. package/templates/naome-root/.naome/task-state.json +8 -0
  36. package/templates/naome-root/.naome/upgrade-state.json +7 -0
  37. package/templates/naome-root/.naome/verification.json +45 -0
  38. package/templates/naome-root/.naomeignore +4 -0
  39. package/templates/naome-root/AGENTS.md +77 -0
  40. package/templates/naome-root/docs/naome/agent-workflow.md +82 -0
  41. package/templates/naome-root/docs/naome/architecture.md +37 -0
  42. package/templates/naome-root/docs/naome/decisions.md +18 -0
  43. package/templates/naome-root/docs/naome/execution.md +192 -0
  44. package/templates/naome-root/docs/naome/first-run.md +135 -0
  45. package/templates/naome-root/docs/naome/index.md +67 -0
  46. package/templates/naome-root/docs/naome/repo-profile.md +51 -0
  47. package/templates/naome-root/docs/naome/security.md +60 -0
  48. package/templates/naome-root/docs/naome/testing.md +51 -0
  49. package/templates/naome-root/docs/naome/upgrade.md +20 -0
@@ -0,0 +1,297 @@
1
+ use std::fs;
2
+ use std::sync::Mutex;
3
+ use std::path::{Path, PathBuf};
4
+ use std::process::Command;
5
+ use std::time::{SystemTime, UNIX_EPOCH};
6
+
7
+ use naome_core::{evaluate_decision, EvaluationOptions};
8
+ use serde_json::json;
9
+
10
+ static ENV_LOCK: Mutex<()> = Mutex::new(());
11
+
12
+ #[test]
13
+ fn first_run_state_returns_protocol_action() {
14
+ let repo = TestRepo::new("first-run");
15
+ repo.write_naome_json("init-state.json", json!({ "initialized": false }));
16
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
17
+ repo.write_naome_json(
18
+ "task-state.json",
19
+ json!({ "status": "idle", "activeTask": null }),
20
+ );
21
+
22
+ let decision = evaluate_decision(repo.path(), EvaluationOptions::offline()).unwrap();
23
+
24
+ assert_eq!(decision.state, "first_run_required");
25
+ assert_eq!(decision.allowed_actions, vec!["run_first_run_protocol"]);
26
+ assert_eq!(
27
+ decision.next_action,
28
+ "Run the NAOME first-run protocol before feature work."
29
+ );
30
+ }
31
+
32
+ #[test]
33
+ fn completed_task_diff_returns_baseline_choices() {
34
+ let repo = TestRepo::new("completed-task");
35
+ repo.init_git();
36
+ repo.write_file("README.md", "# Baseline\n");
37
+ repo.write_naome_json("init-state.json", json!({ "initialized": true }));
38
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
39
+ repo.write_naome_json(
40
+ "task-state.json",
41
+ json!({
42
+ "status": "complete",
43
+ "activeTask": {
44
+ "id": "readme-task",
45
+ "allowedPaths": ["README.md"],
46
+ "requiredCheckIds": ["diff-check"],
47
+ "proofResults": []
48
+ }
49
+ }),
50
+ );
51
+ repo.git(&["add", "."]);
52
+ repo.git(&["commit", "-m", "baseline"]);
53
+ repo.write_file("README.md", "# Changed\n");
54
+
55
+ let decision = evaluate_decision(repo.path(), EvaluationOptions::offline()).unwrap();
56
+
57
+ assert_eq!(decision.state, "completed_task_unbaselined");
58
+ assert!(decision
59
+ .allowed_actions
60
+ .contains(&"commit_task_baseline".to_string()));
61
+ assert!(decision
62
+ .allowed_actions
63
+ .contains(&"request_task_changes".to_string()));
64
+ assert_eq!(decision.changed_paths, vec!["README.md"]);
65
+ }
66
+
67
+ #[test]
68
+ fn idle_unowned_diff_blocks_new_feature_work() {
69
+ let repo = TestRepo::new("unowned-diff");
70
+ repo.init_git();
71
+ repo.write_file("README.md", "# Baseline\n");
72
+ repo.write_naome_json("init-state.json", json!({ "initialized": true }));
73
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
74
+ repo.write_naome_json(
75
+ "task-state.json",
76
+ json!({ "status": "idle", "activeTask": null }),
77
+ );
78
+ repo.git(&["add", "."]);
79
+ repo.git(&["commit", "-m", "baseline"]);
80
+ repo.write_file("README.md", "# Manual change\n");
81
+
82
+ let decision = evaluate_decision(repo.path(), EvaluationOptions::offline()).unwrap();
83
+
84
+ assert_eq!(decision.state, "dirty_unowned_diff");
85
+ assert_eq!(
86
+ decision.allowed_actions,
87
+ vec!["review_unowned_diff", "clear_or_commit_unowned_diff"]
88
+ );
89
+ assert_eq!(decision.changed_paths, vec!["README.md"]);
90
+ }
91
+
92
+ #[test]
93
+ fn changed_paths_handle_worktree_renames_without_corrupting_old_path_tokens() {
94
+ let repo = TestRepo::new("worktree-rename");
95
+ repo.init_git();
96
+ repo.write_file("packages/old/file.txt", "baseline\n");
97
+ repo.write_naome_json("init-state.json", json!({ "initialized": true }));
98
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
99
+ repo.write_naome_json(
100
+ "task-state.json",
101
+ json!({ "status": "idle", "activeTask": null }),
102
+ );
103
+ repo.git(&["add", "."]);
104
+ repo.git(&["commit", "-m", "baseline"]);
105
+
106
+ fs::create_dir_all(repo.path().join("packages/new")).unwrap();
107
+ fs::rename(
108
+ repo.path().join("packages/old/file.txt"),
109
+ repo.path().join("packages/new/file.txt"),
110
+ )
111
+ .unwrap();
112
+ repo.git(&["add", "-N", "packages/new/file.txt"]);
113
+
114
+ let decision = evaluate_decision(repo.path(), EvaluationOptions::offline()).unwrap();
115
+
116
+ assert!(decision
117
+ .changed_paths
118
+ .contains(&"packages/new/file.txt".to_string()));
119
+ assert!(!decision
120
+ .changed_paths
121
+ .iter()
122
+ .any(|path| path == "ckages/old/file.txt" || path == "kages/old/file.txt"));
123
+ }
124
+
125
+ #[test]
126
+ fn active_task_allows_control_state_and_untracked_files_inside_scope() {
127
+ let repo = TestRepo::new("active-scope");
128
+ repo.init_git();
129
+ repo.write_file("README.md", "# Baseline\n");
130
+ repo.write_naome_json("init-state.json", json!({ "initialized": true }));
131
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
132
+ repo.write_naome_json(
133
+ "task-state.json",
134
+ json!({
135
+ "status": "implementing",
136
+ "activeTask": {
137
+ "id": "rust-task",
138
+ "allowedPaths": ["packages/naome/crates/naome-core/src/lib.rs"],
139
+ "requiredCheckIds": ["decision-engine-tests"],
140
+ "proofResults": []
141
+ }
142
+ }),
143
+ );
144
+ repo.git(&["add", "."]);
145
+ repo.git(&["commit", "-m", "baseline"]);
146
+ repo.write_file(
147
+ "packages/naome/crates/naome-core/src/lib.rs",
148
+ "pub fn marker() {}\n",
149
+ );
150
+ repo.write_naome_json(
151
+ "task-state.json",
152
+ json!({
153
+ "status": "implementing",
154
+ "activeTask": {
155
+ "id": "rust-task",
156
+ "allowedPaths": ["packages/naome/crates/naome-core/src/lib.rs"],
157
+ "requiredCheckIds": ["decision-engine-tests"],
158
+ "proofResults": []
159
+ }
160
+ }),
161
+ );
162
+
163
+ let decision = evaluate_decision(repo.path(), EvaluationOptions::offline()).unwrap();
164
+
165
+ assert_eq!(decision.state, "active_task_in_progress");
166
+ assert!(!decision.blocked);
167
+ assert_eq!(
168
+ decision.changed_paths,
169
+ vec!["packages/naome/crates/naome-core/src/lib.rs".to_string()]
170
+ );
171
+ }
172
+
173
+ #[test]
174
+ fn changed_paths_exclude_naomeignore_matches() {
175
+ let repo = TestRepo::new("ignored-archive");
176
+ repo.init_git();
177
+ repo.write_file(".naomeignore", ".naome/archive/\n");
178
+ repo.write_naome_json("init-state.json", json!({ "initialized": false }));
179
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
180
+ repo.write_naome_json(
181
+ "task-state.json",
182
+ json!({ "status": "idle", "activeTask": null }),
183
+ );
184
+ repo.git(&["add", "."]);
185
+ repo.git(&["commit", "-m", "baseline"]);
186
+ repo.write_file(
187
+ ".naome/archive/AGENTS.pre-naome.md",
188
+ "# Archived instructions\n",
189
+ );
190
+
191
+ let decision = evaluate_decision(repo.path(), EvaluationOptions::offline()).unwrap();
192
+
193
+ assert_eq!(decision.state, "first_run_required");
194
+ assert!(decision.changed_paths.is_empty());
195
+ }
196
+
197
+ #[cfg(unix)]
198
+ #[test]
199
+ fn online_checks_use_explicit_node_binary_from_environment() {
200
+ let _guard = ENV_LOCK.lock().unwrap();
201
+ let repo = TestRepo::new("explicit-node-bin");
202
+ repo.init_git();
203
+ repo.write_naome_json("init-state.json", json!({ "initialized": true }));
204
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
205
+ repo.write_naome_json(
206
+ "task-state.json",
207
+ json!({ "status": "idle", "activeTask": null }),
208
+ );
209
+ repo.git(&["add", "."]);
210
+ repo.git(&["commit", "-m", "baseline"]);
211
+ let fake_node = std::env::temp_dir().join(format!(
212
+ "naome-fake-node-{}-{}",
213
+ std::process::id(),
214
+ SystemTime::now()
215
+ .duration_since(UNIX_EPOCH)
216
+ .unwrap()
217
+ .as_nanos()
218
+ ));
219
+ fs::write(&fake_node, "#!/bin/sh\nexit 0\n").unwrap();
220
+ let mut permissions = fs::metadata(&fake_node).unwrap().permissions();
221
+ use std::os::unix::fs::PermissionsExt;
222
+ permissions.set_mode(0o755);
223
+ fs::set_permissions(&fake_node, permissions).unwrap();
224
+
225
+ let previous = std::env::var_os("NAOME_NODE_BIN");
226
+ std::env::set_var("NAOME_NODE_BIN", &fake_node);
227
+ let decision = evaluate_decision(repo.path(), EvaluationOptions::online()).unwrap();
228
+ if let Some(value) = previous {
229
+ std::env::set_var("NAOME_NODE_BIN", value);
230
+ } else {
231
+ std::env::remove_var("NAOME_NODE_BIN");
232
+ }
233
+
234
+ assert_eq!(decision.state, "ready_for_task");
235
+ assert_eq!(
236
+ decision.harness_health.unwrap().command,
237
+ format!("{} .naome/bin/check-harness-health.js", fake_node.display())
238
+ );
239
+ }
240
+
241
+ struct TestRepo {
242
+ root: PathBuf,
243
+ }
244
+
245
+ impl TestRepo {
246
+ fn new(name: &str) -> Self {
247
+ let nonce = SystemTime::now()
248
+ .duration_since(UNIX_EPOCH)
249
+ .unwrap()
250
+ .as_nanos();
251
+ let root = std::env::temp_dir().join(format!("naome-core-{name}-{nonce}"));
252
+ fs::create_dir_all(root.join(".naome")).unwrap();
253
+ Self { root }
254
+ }
255
+
256
+ fn path(&self) -> &Path {
257
+ &self.root
258
+ }
259
+
260
+ fn write_file(&self, relative_path: &str, content: &str) {
261
+ let path = self.root.join(relative_path);
262
+ if let Some(parent) = path.parent() {
263
+ fs::create_dir_all(parent).unwrap();
264
+ }
265
+ fs::write(path, content).unwrap();
266
+ }
267
+
268
+ fn write_naome_json(&self, file_name: &str, value: serde_json::Value) {
269
+ let path = self.root.join(".naome").join(file_name);
270
+ fs::write(
271
+ path,
272
+ format!("{}\n", serde_json::to_string_pretty(&value).unwrap()),
273
+ )
274
+ .unwrap();
275
+ }
276
+
277
+ fn init_git(&self) {
278
+ self.git(&["init"]);
279
+ self.git(&["config", "user.email", "naome@example.com"]);
280
+ self.git(&["config", "user.name", "NAOME Test"]);
281
+ }
282
+
283
+ fn git(&self, args: &[&str]) {
284
+ let output = Command::new("git")
285
+ .args(args)
286
+ .current_dir(&self.root)
287
+ .output()
288
+ .unwrap();
289
+ assert!(
290
+ output.status.success(),
291
+ "git {:?} failed\nstdout:\n{}\nstderr:\n{}",
292
+ args,
293
+ String::from_utf8_lossy(&output.stdout),
294
+ String::from_utf8_lossy(&output.stderr)
295
+ );
296
+ }
297
+ }
@@ -0,0 +1,232 @@
1
+ use std::collections::HashMap;
2
+ use std::fs;
3
+ use std::path::{Path, PathBuf};
4
+ use std::sync::atomic::{AtomicU64, Ordering};
5
+ use std::time::{SystemTime, UNIX_EPOCH};
6
+
7
+ use naome_core::{validate_harness_health, HarnessHealthOptions};
8
+ use serde_json::json;
9
+ use sha2::{Digest, Sha256};
10
+
11
+ const MACHINE_OWNED_PATHS: &[&str] = &[
12
+ "AGENTS.md",
13
+ ".naome/package.json",
14
+ ".naome/bin/naome.js",
15
+ ".naome/bin/check-task-state.js",
16
+ ".naome/bin/check-harness-health.js",
17
+ ".naome/task-contract.schema.json",
18
+ "docs/naome/index.md",
19
+ "docs/naome/first-run.md",
20
+ "docs/naome/agent-workflow.md",
21
+ "docs/naome/execution.md",
22
+ "docs/naome/upgrade.md",
23
+ ];
24
+
25
+ const PROJECT_OWNED_PATHS: &[&str] = &[
26
+ ".naomeignore",
27
+ ".naome/init-state.json",
28
+ ".naome/manifest.json",
29
+ ".naome/task-state.json",
30
+ ".naome/upgrade-state.json",
31
+ ".naome/verification.json",
32
+ "docs/naome/architecture.md",
33
+ "docs/naome/decisions.md",
34
+ "docs/naome/repo-profile.md",
35
+ "docs/naome/security.md",
36
+ "docs/naome/testing.md",
37
+ ];
38
+
39
+ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
40
+
41
+ #[test]
42
+ fn accepts_healthy_harness_root() {
43
+ let repo = HarnessFixture::new();
44
+
45
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
46
+
47
+ assert!(errors.is_empty(), "{errors:#?}");
48
+ }
49
+
50
+ #[test]
51
+ fn accepts_missing_archive_directory_when_ignore_boundary_exists() {
52
+ let repo = HarnessFixture::new();
53
+ fs::remove_dir_all(repo.path().join(".naome/archive")).unwrap();
54
+
55
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
56
+
57
+ assert!(errors.is_empty(), "{errors:#?}");
58
+ }
59
+
60
+ #[test]
61
+ fn rejects_drifted_machine_owned_files() {
62
+ let repo = HarnessFixture::new();
63
+ repo.write("docs/naome/execution.md", "# Tampered\n");
64
+
65
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
66
+ let joined = errors.join("\n");
67
+
68
+ assert!(joined.contains("integrity mismatch"));
69
+ assert!(joined.contains("docs/naome/execution.md"));
70
+ }
71
+
72
+ #[test]
73
+ fn rejects_missing_archive_ignore_boundary() {
74
+ let repo = HarnessFixture::new();
75
+ repo.write(".naomeignore", "node_modules/\n");
76
+
77
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
78
+ let joined = errors.join("\n");
79
+
80
+ assert!(joined.contains(".naomeignore must include .naome/archive/."));
81
+ }
82
+
83
+ #[cfg(unix)]
84
+ #[test]
85
+ fn rejects_symlinked_machine_owned_paths() {
86
+ let repo = HarnessFixture::new();
87
+ repo.write("external-execution.md", "# External\n");
88
+ fs::remove_file(repo.path().join("docs/naome/execution.md")).unwrap();
89
+ std::os::unix::fs::symlink(
90
+ repo.path().join("external-execution.md"),
91
+ repo.path().join("docs/naome/execution.md"),
92
+ )
93
+ .unwrap();
94
+
95
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
96
+ let joined = errors.join("\n");
97
+
98
+ assert!(joined.contains("must not contain symlinks"));
99
+ assert!(joined.contains("docs/naome/execution.md"));
100
+ }
101
+
102
+ fn options(expected_integrity: HashMap<String, String>) -> HarnessHealthOptions {
103
+ HarnessHealthOptions {
104
+ expected_integrity,
105
+ allow_missing_integrity: false,
106
+ allow_missing_archive: false,
107
+ }
108
+ }
109
+
110
+ struct HarnessFixture {
111
+ root: PathBuf,
112
+ integrity: HashMap<String, String>,
113
+ }
114
+
115
+ impl HarnessFixture {
116
+ fn new() -> Self {
117
+ let nonce = SystemTime::now()
118
+ .duration_since(UNIX_EPOCH)
119
+ .unwrap()
120
+ .as_nanos();
121
+ let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
122
+ let root = std::env::temp_dir().join(format!(
123
+ "naome-harness-health-{}-{nonce}-{counter}",
124
+ std::process::id()
125
+ ));
126
+ let mut integrity = HashMap::new();
127
+
128
+ for relative_path in MACHINE_OWNED_PATHS {
129
+ let content = machine_content(relative_path);
130
+ write_file(&root, relative_path, &content);
131
+ integrity.insert(
132
+ (*relative_path).to_string(),
133
+ format!("sha256:{}", sha256(&content)),
134
+ );
135
+ }
136
+
137
+ for relative_path in PROJECT_OWNED_PATHS {
138
+ let content = if *relative_path == ".naomeignore" {
139
+ ".naome/archive/\n".to_string()
140
+ } else {
141
+ format!("# {relative_path}\n")
142
+ };
143
+ write_file(&root, relative_path, &content);
144
+ }
145
+
146
+ fs::create_dir_all(root.join(".naome/archive")).unwrap();
147
+ write_file(
148
+ &root,
149
+ ".naome/manifest.json",
150
+ &format!(
151
+ "{}\n",
152
+ serde_json::to_string_pretty(&json!({
153
+ "name": "naome",
154
+ "harnessVersion": "0.6.1",
155
+ "profile": "standard",
156
+ "installedAt": "2026-05-05T00:00:00.000Z",
157
+ "machineOwned": MACHINE_OWNED_PATHS,
158
+ "projectOwned": PROJECT_OWNED_PATHS,
159
+ "integrity": integrity
160
+ }))
161
+ .unwrap()
162
+ ),
163
+ );
164
+ write_file(
165
+ &root,
166
+ ".naome/task-state.json",
167
+ &format!(
168
+ "{}\n",
169
+ serde_json::to_string_pretty(&json!({
170
+ "schema": "naome.task-state.v1",
171
+ "version": 1,
172
+ "status": "idle",
173
+ "activeTask": null,
174
+ "blocker": null,
175
+ "updatedAt": null
176
+ }))
177
+ .unwrap()
178
+ ),
179
+ );
180
+ write_file(
181
+ &root,
182
+ ".naome/upgrade-state.json",
183
+ &format!(
184
+ "{}\n",
185
+ serde_json::to_string_pretty(&json!({
186
+ "status": "complete",
187
+ "fromVersion": null,
188
+ "toVersion": "0.6.1",
189
+ "pending": [],
190
+ "completed": []
191
+ }))
192
+ .unwrap()
193
+ ),
194
+ );
195
+
196
+ Self { root, integrity }
197
+ }
198
+
199
+ fn path(&self) -> &Path {
200
+ &self.root
201
+ }
202
+
203
+ fn write(&self, relative_path: &str, content: &str) {
204
+ write_file(&self.root, relative_path, content);
205
+ }
206
+ }
207
+
208
+ fn machine_content(relative_path: &str) -> String {
209
+ match relative_path {
210
+ "AGENTS.md" => [
211
+ "# Agent Instructions",
212
+ "Run node .naome/bin/check-harness-health.js before feature work.",
213
+ "Run node .naome/bin/check-task-state.js --admission before task work.",
214
+ "",
215
+ ]
216
+ .join("\n"),
217
+ ".naome/package.json" => format!("{}\n", json!({ "type": "commonjs" })),
218
+ _ => format!("# {relative_path}\n"),
219
+ }
220
+ }
221
+
222
+ fn write_file(root: &Path, relative_path: &str, content: &str) {
223
+ let path = root.join(relative_path);
224
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
225
+ fs::write(path, content).unwrap();
226
+ }
227
+
228
+ fn sha256(content: &str) -> String {
229
+ let mut hasher = Sha256::new();
230
+ hasher.update(content.as_bytes());
231
+ format!("{:x}", hasher.finalize())
232
+ }
@@ -0,0 +1,35 @@
1
+ use naome_core::install_plan;
2
+
3
+ #[test]
4
+ fn install_plan_marks_machine_docs_and_bins_local_only() {
5
+ let plan = install_plan("1.0.0");
6
+
7
+ assert_eq!(plan.schema, "naome.install-plan.v1");
8
+ assert_eq!(plan.harness_version, "1.0.0");
9
+ assert!(plan.machine_owned.contains(&"docs/naome/execution.md"));
10
+ assert!(plan.project_owned.contains(&"docs/naome/architecture.md"));
11
+ assert!(plan
12
+ .local_only_machine_owned
13
+ .contains(&".naome/bin/check-task-state.js"));
14
+ assert!(plan
15
+ .local_only_machine_owned
16
+ .contains(&"docs/naome/execution.md"));
17
+ assert!(!plan.local_only_machine_owned.contains(&"AGENTS.md"));
18
+ assert!(!plan
19
+ .local_only_machine_owned
20
+ .contains(&"docs/naome/architecture.md"));
21
+ }
22
+
23
+ #[test]
24
+ fn install_plan_includes_gitignore_and_untrack_policy() {
25
+ let plan = install_plan("1.0.0");
26
+
27
+ assert!(plan.gitignore_entries.contains(&".naome/archive/"));
28
+ assert!(plan.gitignore_entries.contains(&".naome/bin/naome-rust*"));
29
+ assert!(plan
30
+ .gitignore_entries
31
+ .contains(&".naome/bin/check-harness-health.js"));
32
+ assert!(plan.git_untrack_paths.contains(&".naome/archive"));
33
+ assert!(plan.git_untrack_paths.contains(&".naome/bin/naome-rust"));
34
+ assert!(plan.git_untrack_paths.contains(&"docs/naome/first-run.md"));
35
+ }