@lamentis/naome 1.3.8 → 1.3.10

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 (53) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +5 -0
  3. package/bin/naome.js +1 -1
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/architecture_commands.rs +123 -0
  6. package/crates/naome-cli/src/cli_args.rs +4 -0
  7. package/crates/naome-cli/src/dispatcher.rs +2 -0
  8. package/crates/naome-cli/src/install_bridge.rs +56 -8
  9. package/crates/naome-cli/src/main.rs +6 -0
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
  12. package/crates/naome-core/src/architecture/config/parser/sections.rs +137 -0
  13. package/crates/naome-core/src/architecture/config/parser.rs +96 -0
  14. package/crates/naome-core/src/architecture/config.rs +114 -0
  15. package/crates/naome-core/src/architecture/model.rs +80 -0
  16. package/crates/naome-core/src/architecture/output.rs +178 -0
  17. package/crates/naome-core/src/architecture/rules.rs +140 -0
  18. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +56 -0
  19. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +88 -0
  20. package/crates/naome-core/src/architecture/scan/graph_builder.rs +134 -0
  21. package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
  22. package/crates/naome-core/src/architecture/scan.rs +75 -0
  23. package/crates/naome-core/src/architecture.rs +31 -0
  24. package/crates/naome-core/src/harness_health/integrity.rs +41 -23
  25. package/crates/naome-core/src/harness_health/manifest.rs +97 -0
  26. package/crates/naome-core/src/harness_health.rs +58 -106
  27. package/crates/naome-core/src/install_plan.rs +2 -0
  28. package/crates/naome-core/src/lib.rs +16 -8
  29. package/crates/naome-core/src/quality/cache.rs +122 -19
  30. package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
  31. package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
  32. package/crates/naome-core/src/quality/scanner.rs +5 -2
  33. package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
  34. package/crates/naome-core/tests/architecture.rs +209 -0
  35. package/crates/naome-core/tests/harness_health.rs +150 -0
  36. package/crates/naome-core/tests/quality_performance.rs +63 -2
  37. package/installer/filesystem.js +38 -0
  38. package/installer/flows.js +6 -1
  39. package/installer/harness-file-ops.js +36 -8
  40. package/installer/harness-files.js +3 -0
  41. package/installer/manifest-state.js +2 -2
  42. package/installer/native.js +63 -18
  43. package/native/darwin-arm64/naome +0 -0
  44. package/native/linux-x64/naome +0 -0
  45. package/package.json +1 -1
  46. package/templates/naome-root/.naome/bin/check-harness-health.js +23 -19
  47. package/templates/naome-root/.naome/bin/check-task-state.js +33 -40
  48. package/templates/naome-root/.naome/bin/naome.js +2 -2
  49. package/templates/naome-root/.naome/manifest.json +8 -6
  50. package/templates/naome-root/.naome/verification.json +15 -1
  51. package/templates/naome-root/docs/naome/architecture-fitness.md +97 -0
  52. package/templates/naome-root/docs/naome/index.md +4 -3
  53. package/templates/naome-root/docs/naome/testing.md +6 -3
@@ -0,0 +1,209 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+ use std::sync::atomic::{AtomicU64, Ordering};
5
+ use std::time::{SystemTime, UNIX_EPOCH};
6
+
7
+ use naome_core::{
8
+ default_architecture_config_text, scan_architecture, validate_architecture, ArchitectureConfig,
9
+ ArchitectureScanOptions,
10
+ };
11
+
12
+ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
13
+
14
+ #[test]
15
+ fn parses_starter_architecture_config() {
16
+ let config = ArchitectureConfig::parse(default_architecture_config_text(), "test").unwrap();
17
+
18
+ assert!(config.layers.contains_key("application"));
19
+ assert_eq!(
20
+ config.rule("max_file_lines").value,
21
+ Some(400),
22
+ "starter config should seed a production file-size budget"
23
+ );
24
+ assert_eq!(
25
+ config.ignore[0].reason,
26
+ "Generated code is not architecture-owned."
27
+ );
28
+ }
29
+
30
+ #[test]
31
+ fn path_extractor_builds_language_agnostic_graph_for_mixed_repo() {
32
+ let repo = FixtureRepo::new();
33
+ repo.write("src/domain/event.ts", "export const event = 1;\n");
34
+ repo.write("src/infrastructure/db.py", "client = object()\n");
35
+ repo.write("cmd/server/main.go", "package main\n");
36
+ repo.write(
37
+ "naome.arch.yaml",
38
+ r#"
39
+ layers:
40
+ domain:
41
+ paths:
42
+ - "src/domain/**"
43
+ infrastructure:
44
+ paths:
45
+ - "src/infrastructure/**"
46
+ contexts:
47
+ app:
48
+ paths:
49
+ - "src/**"
50
+ public_api:
51
+ - "src/index.ts"
52
+ rules:
53
+ max_file_lines:
54
+ enabled: true
55
+ value: 400
56
+ severity: warning
57
+ "#,
58
+ );
59
+
60
+ let scan = scan_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
61
+
62
+ assert!(scan
63
+ .graph
64
+ .nodes
65
+ .iter()
66
+ .any(|node| node.id == "file:src/domain/event.ts"));
67
+ assert!(scan
68
+ .graph
69
+ .nodes
70
+ .iter()
71
+ .any(|node| node.id == "layer:domain"));
72
+ assert!(scan
73
+ .graph
74
+ .edges
75
+ .iter()
76
+ .any(|edge| { edge.from == "layer:domain" && edge.to == "file:src/domain/event.ts" }));
77
+ assert_eq!(
78
+ scan.file_facts
79
+ .get("cmd/server/main.go")
80
+ .unwrap()
81
+ .language
82
+ .as_deref(),
83
+ Some("go")
84
+ );
85
+ }
86
+
87
+ #[test]
88
+ fn validates_file_size_budget_with_stable_json_shape() {
89
+ let repo = FixtureRepo::new();
90
+ repo.write(
91
+ "naome.arch.yaml",
92
+ r#"
93
+ rules:
94
+ max_file_lines:
95
+ enabled: true
96
+ value: 2
97
+ severity: error
98
+ "#,
99
+ );
100
+ repo.write("src/too_big.rs", "one\ntwo\nthree\n");
101
+
102
+ let report = validate_architecture(repo.path(), ArchitectureScanOptions::default()).unwrap();
103
+ let json = serde_json::to_string(&report).unwrap();
104
+
105
+ assert_eq!(report.status, "fail");
106
+ assert!(json.contains("\"schema\":\"naome.arch.validation.v1\""));
107
+ assert!(json.contains("\"id\":\"arch.max_file_lines\""));
108
+ let violation = report
109
+ .violations
110
+ .iter()
111
+ .find(|violation| violation.path.as_deref() == Some("src/too_big.rs"))
112
+ .expect("expected src/too_big.rs file-size violation");
113
+ assert_eq!(violation.agent_instruction, "Reduce src/too_big.rs below 2 lines or add a justified generated-code ignore rule if it is not manually owned.");
114
+ }
115
+
116
+ #[test]
117
+ fn changed_only_generated_boundary_reports_changed_generated_file() {
118
+ let repo = FixtureRepo::new();
119
+ repo.init_git();
120
+ repo.write(
121
+ "naome.arch.yaml",
122
+ r#"
123
+ rules:
124
+ generated_manual_boundary:
125
+ enabled: true
126
+ severity: error
127
+ ignore:
128
+ - path: "generated/**"
129
+ reason: "Generated code is not manually edited."
130
+ "#,
131
+ );
132
+ repo.write("generated/client.ts", "export const generated = true;\n");
133
+
134
+ let report = validate_architecture(
135
+ repo.path(),
136
+ ArchitectureScanOptions {
137
+ changed_only: true,
138
+ config_path: None,
139
+ },
140
+ )
141
+ .unwrap();
142
+
143
+ assert_eq!(report.status, "fail");
144
+ assert!(report.changed_only_degraded_to_full_scan);
145
+ assert_eq!(report.violations[0].id, "arch.generated_manual_boundary");
146
+ assert_eq!(
147
+ report.violations[0].path.as_deref(),
148
+ Some("generated/client.ts")
149
+ );
150
+ }
151
+
152
+ struct FixtureRepo {
153
+ root: PathBuf,
154
+ }
155
+
156
+ impl FixtureRepo {
157
+ fn new() -> Self {
158
+ let nonce = SystemTime::now()
159
+ .duration_since(UNIX_EPOCH)
160
+ .unwrap()
161
+ .as_nanos();
162
+ let counter = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed);
163
+ let root = std::env::temp_dir().join(format!(
164
+ "naome-arch-fixture-{}-{nonce}-{counter}",
165
+ std::process::id()
166
+ ));
167
+ fs::create_dir_all(&root).unwrap();
168
+ fs::write(
169
+ root.join(".naomeignore"),
170
+ ".naome/archive/\n.naome/tasks/\n",
171
+ )
172
+ .unwrap();
173
+ Self { root }
174
+ }
175
+
176
+ fn path(&self) -> &Path {
177
+ &self.root
178
+ }
179
+
180
+ fn write(&self, relative_path: &str, content: &str) {
181
+ let path = self.root.join(relative_path);
182
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
183
+ fs::write(path, content).unwrap();
184
+ }
185
+
186
+ fn init_git(&self) {
187
+ run_git(&self.root, &["init"]);
188
+ run_git(&self.root, &["config", "user.email", "naome@example.com"]);
189
+ run_git(&self.root, &["config", "user.name", "NAOME Test"]);
190
+ self.write("README.md", "# Fixture\n");
191
+ run_git(&self.root, &["add", "."]);
192
+ run_git(&self.root, &["commit", "-m", "baseline"]);
193
+ }
194
+ }
195
+
196
+ fn run_git(root: &Path, args: &[&str]) {
197
+ let result = Command::new("git")
198
+ .args(args)
199
+ .current_dir(root)
200
+ .output()
201
+ .unwrap();
202
+ assert!(
203
+ result.status.success(),
204
+ "git {:?} failed: {}{}",
205
+ args,
206
+ String::from_utf8_lossy(&result.stdout),
207
+ String::from_utf8_lossy(&result.stderr)
208
+ );
209
+ }
@@ -19,11 +19,17 @@ const MACHINE_OWNED_PATHS: &[&str] = &[
19
19
  "docs/naome/first-run.md",
20
20
  "docs/naome/agent-workflow.md",
21
21
  "docs/naome/context-economy.md",
22
+ "docs/naome/architecture-fitness.md",
22
23
  "docs/naome/execution.md",
23
24
  "docs/naome/task-ledger.md",
24
25
  "docs/naome/upgrade.md",
25
26
  ];
26
27
 
28
+ #[cfg(windows)]
29
+ const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust.exe";
30
+ #[cfg(not(windows))]
31
+ const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust";
32
+
27
33
  const PROJECT_OWNED_PATHS: &[&str] = &[
28
34
  ".naomeignore",
29
35
  ".naome/init-state.json",
@@ -78,6 +84,99 @@ fn rejects_drifted_machine_owned_files() {
78
84
  assert!(joined.contains("docs/naome/execution.md"));
79
85
  }
80
86
 
87
+ #[test]
88
+ fn accepts_native_decision_binary_with_manifest_ownership_and_integrity() {
89
+ let mut repo = HarnessFixture::new();
90
+ repo.install_native_decision_binary("native decision fixture\n");
91
+
92
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
93
+
94
+ assert!(errors.is_empty(), "{errors:#?}");
95
+ }
96
+
97
+ #[test]
98
+ fn rejects_native_decision_binary_when_manifest_ownership_is_removed() {
99
+ let mut repo = HarnessFixture::new();
100
+ repo.install_native_decision_binary("native decision fixture\n");
101
+ repo.remove_native_manifest_entry();
102
+
103
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
104
+ let joined = errors.join("\n");
105
+
106
+ assert!(
107
+ joined.contains(&format!(
108
+ ".naome/manifest.json machineOwned must include {NATIVE_BINARY_PATH}."
109
+ )),
110
+ "{joined}"
111
+ );
112
+ assert!(
113
+ joined.contains(&format!(
114
+ ".naome/manifest.json integrity missing {NATIVE_BINARY_PATH}."
115
+ )),
116
+ "{joined}"
117
+ );
118
+ }
119
+
120
+ #[test]
121
+ fn rejects_checker_declared_native_integrity_without_native_manifest_entry() {
122
+ let mut repo = HarnessFixture::new();
123
+ let checker_path = ".naome/bin/check-task-state.js";
124
+ let native_hash = format!("sha256:{}", "a".repeat(64));
125
+ repo.write(
126
+ checker_path,
127
+ &format!("const expectedNativeBinaryIntegrity = \"{native_hash}\";\n"),
128
+ );
129
+ repo.integrity.insert(
130
+ checker_path.to_string(),
131
+ format!(
132
+ "sha256:{}",
133
+ sha256("const expectedNativeBinaryIntegrity = \"sha256:generated\";\n")
134
+ ),
135
+ );
136
+ write_file(
137
+ repo.path(),
138
+ ".naome/manifest.json",
139
+ &pretty_json(manifest_fixture(&repo.integrity)),
140
+ );
141
+
142
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
143
+ let joined = errors.join("\n");
144
+
145
+ assert!(
146
+ joined.contains(&format!(
147
+ ".naome/manifest.json machineOwned must include {NATIVE_BINARY_PATH}."
148
+ )),
149
+ "{joined}"
150
+ );
151
+ assert!(
152
+ joined.contains(&format!("{NATIVE_BINARY_PATH} is missing.")),
153
+ "{joined}"
154
+ );
155
+ }
156
+
157
+ #[test]
158
+ fn rejects_native_integrity_assignment_with_appended_code() {
159
+ let mut repo = HarnessFixture::new();
160
+ repo.install_native_decision_binary("native decision fixture\n");
161
+ let native_hash = repo.integrity.get(NATIVE_BINARY_PATH).unwrap().clone();
162
+ repo.write(
163
+ ".naome/bin/check-harness-health.js",
164
+ &format!("const expectedNativeBinaryIntegrity = \"{native_hash}\"; process.exit(0);\n"),
165
+ );
166
+
167
+ let errors = validate_harness_health(repo.path(), options(repo.integrity.clone())).unwrap();
168
+ let joined = errors.join("\n");
169
+
170
+ assert!(
171
+ joined.contains(".naome/bin/check-harness-health.js integrity mismatch"),
172
+ "{joined}"
173
+ );
174
+ assert!(
175
+ joined.contains(".naome/bin/check-harness-health.js native binary integrity does not match .naome/manifest.json."),
176
+ "{joined}"
177
+ );
178
+ }
179
+
81
180
  #[test]
82
181
  fn rejects_missing_archive_ignore_boundary() {
83
182
  let repo = HarnessFixture::new();
@@ -175,6 +274,53 @@ impl HarnessFixture {
175
274
  fn write(&self, relative_path: &str, content: &str) {
176
275
  write_file(&self.root, relative_path, content);
177
276
  }
277
+
278
+ fn install_native_decision_binary(&mut self, content: &str) {
279
+ let native_hash = format!("sha256:{}", sha256(content));
280
+ write_file(&self.root, NATIVE_BINARY_PATH, content);
281
+ self.integrity
282
+ .insert(NATIVE_BINARY_PATH.to_string(), native_hash.clone());
283
+
284
+ for relative_path in [
285
+ ".naome/bin/naome.js",
286
+ ".naome/bin/check-harness-health.js",
287
+ ".naome/bin/check-task-state.js",
288
+ ] {
289
+ let command_content =
290
+ format!("const expectedNativeBinaryIntegrity = \"{native_hash}\";\n");
291
+ let normalized_command_content =
292
+ "const expectedNativeBinaryIntegrity = \"sha256:generated\";\n";
293
+ write_file(&self.root, relative_path, &command_content);
294
+ self.integrity.insert(
295
+ relative_path.to_string(),
296
+ format!("sha256:{}", sha256(normalized_command_content)),
297
+ );
298
+ }
299
+
300
+ self.write_manifest_with_native_entry();
301
+ }
302
+
303
+ fn remove_native_manifest_entry(&self) {
304
+ let mut manifest = read_json_value(&self.root, ".naome/manifest.json");
305
+ manifest["machineOwned"]
306
+ .as_array_mut()
307
+ .unwrap()
308
+ .retain(|entry| entry.as_str() != Some(NATIVE_BINARY_PATH));
309
+ manifest["integrity"]
310
+ .as_object_mut()
311
+ .unwrap()
312
+ .remove(NATIVE_BINARY_PATH);
313
+ write_file(&self.root, ".naome/manifest.json", &pretty_json(manifest));
314
+ }
315
+
316
+ fn write_manifest_with_native_entry(&self) {
317
+ let mut manifest = manifest_fixture(&self.integrity);
318
+ manifest["machineOwned"]
319
+ .as_array_mut()
320
+ .unwrap()
321
+ .push(json!(NATIVE_BINARY_PATH));
322
+ write_file(&self.root, ".naome/manifest.json", &pretty_json(manifest));
323
+ }
178
324
  }
179
325
 
180
326
  fn manifest_fixture(integrity: &HashMap<String, String>) -> serde_json::Value {
@@ -217,6 +363,10 @@ fn machine_content(relative_path: &str) -> String {
217
363
  }
218
364
  }
219
365
 
366
+ fn read_json_value(root: &Path, relative_path: &str) -> serde_json::Value {
367
+ serde_json::from_str(&fs::read_to_string(root.join(relative_path)).unwrap()).unwrap()
368
+ }
369
+
220
370
  fn write_file(root: &Path, relative_path: &str, content: &str) {
221
371
  let path = root.join(relative_path);
222
372
  fs::create_dir_all(path.parent().unwrap()).unwrap();
@@ -4,8 +4,8 @@ use std::fs;
4
4
 
5
5
  use naome_core::{
6
6
  check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
7
- init_repository_quality, init_repository_quality_with_mode, quality_cache_status,
8
- QualityInitMode, QualityMode,
7
+ clear_quality_cache, init_repository_quality, init_repository_quality_with_mode,
8
+ quality_cache_status, QualityInitMode, QualityMode,
9
9
  };
10
10
 
11
11
  use repo_support::TestRepo;
@@ -140,6 +140,67 @@ fn second_report_uses_file_analysis_cache() {
140
140
  assert_eq!(second.summary.cache_misses, 0);
141
141
  }
142
142
 
143
+ #[cfg(unix)]
144
+ #[test]
145
+ fn report_skips_repository_symlinks_without_caching_target_contents() {
146
+ let repo = quality_repo("quality-cache-skips-file-symlink");
147
+ let victim = repo.path().join("../naome-victim-secret.txt");
148
+ fs::write(&victim, "VALIDATION_SECRET_raw_lines_12345\n").unwrap();
149
+ std::os::unix::fs::symlink(&victim, repo.path().join("leak.txt")).unwrap();
150
+ repo.git(&["add", "leak.txt"]);
151
+ repo.git(&["commit", "-m", "tracked symlink"]);
152
+
153
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
154
+ let cache = quality_cache_status(repo.path()).unwrap();
155
+
156
+ assert!(!report.scanned_paths.contains(&"leak.txt".to_string()));
157
+ assert_eq!(cache.entry_count, 0);
158
+ }
159
+
160
+ #[cfg(unix)]
161
+ #[test]
162
+ fn path_budget_skips_symlinks_before_reading_target_metadata() {
163
+ let repo = quality_repo("quality-budget-skips-symlink");
164
+ let victim = repo.path().join("../naome-large-victim.txt");
165
+ fs::write(&victim, "x".repeat(1024 * 1024 + 1)).unwrap();
166
+ std::os::unix::fs::symlink(&victim, repo.path().join("large-link.txt")).unwrap();
167
+ repo.git(&["add", "large-link.txt"]);
168
+ repo.git(&["commit", "-m", "tracked large symlink"]);
169
+
170
+ let report = check_repository_quality_paths(repo.path(), &["large-link.txt"]).unwrap();
171
+
172
+ assert!(!report.scanned_paths.contains(&"large-link.txt".to_string()));
173
+ assert!(!report
174
+ .summary
175
+ .reason_codes
176
+ .contains(&"max_file_bytes".to_string()));
177
+ fs::remove_file(victim).unwrap();
178
+ }
179
+
180
+ #[cfg(unix)]
181
+ #[test]
182
+ fn cache_operations_reject_symlinked_cache_path_components() {
183
+ let repo = quality_repo("quality-cache-rejects-cache-symlink");
184
+ repo.write_file("src/a.js", "export const value = 1;\n");
185
+ repo.commit_all("baseline");
186
+ let outside = repo.path().join("../naome-outside-cache");
187
+ fs::create_dir_all(outside.join("quality")).unwrap();
188
+ fs::remove_dir_all(repo.path().join(".naome/cache")).ok();
189
+ std::os::unix::fs::symlink(&outside, repo.path().join(".naome/cache")).unwrap();
190
+
191
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
192
+ let clear_error = clear_quality_cache(repo.path()).unwrap_err();
193
+
194
+ assert_eq!(report.summary.cache_hits, 0);
195
+ assert!(outside.join("quality").is_dir());
196
+ assert!(!fs::read_dir(outside.join("quality"))
197
+ .unwrap()
198
+ .any(|entry| entry.is_ok()));
199
+ assert!(clear_error
200
+ .to_string()
201
+ .contains("must not contain symlinks"));
202
+ }
203
+
143
204
  #[test]
144
205
  fn report_budget_marks_truncated_reports() {
145
206
  let repo = quality_repo("quality-report-budget");
@@ -64,6 +64,44 @@ export function archiveUpgradePath(ctx, archiveDirName, relativePath) {
64
64
  return join(ctx.targetRoot, ".naome", "archive", archiveDirName, relativePath);
65
65
  }
66
66
 
67
+ export function assertWritableArchivePath(ctx, archiveDirName, relativePath) {
68
+ const archiveRelativePath = join(".naome", "archive", archiveDirName, relativePath);
69
+ const parts = archiveRelativePath.split(/[\\/]+/);
70
+ let current = ctx.targetRoot;
71
+
72
+ for (let index = 0; index < parts.length; index += 1) {
73
+ const part = parts[index];
74
+ const isLeaf = index === parts.length - 1;
75
+ current = join(current, part);
76
+
77
+ try {
78
+ const stats = lstatSync(current);
79
+ if (stats.isSymbolicLink()) {
80
+ printError(ctx, `NAOME cannot archive ${relativePath} safely.`);
81
+ console.error(`${archiveRelativePath} must not contain symbolic links.`);
82
+ process.exit(1);
83
+ }
84
+
85
+ if (!isLeaf && !stats.isDirectory()) {
86
+ printError(ctx, `NAOME cannot archive ${relativePath} because the archive path is not a directory.`);
87
+ console.error(`${join(...parts.slice(0, index + 1))} must be a regular directory or must not exist.`);
88
+ process.exit(1);
89
+ }
90
+
91
+ if (isLeaf && existsSync(current) && !stats.isFile()) {
92
+ printError(ctx, `NAOME cannot archive ${relativePath} because the archive path is not a file.`);
93
+ console.error(`${archiveRelativePath} must be a regular file or must not exist.`);
94
+ process.exit(1);
95
+ }
96
+ } catch (error) {
97
+ if (error.code === "ENOENT") {
98
+ continue;
99
+ }
100
+ throw error;
101
+ }
102
+ }
103
+ }
104
+
67
105
  export function ensureArchiveDirectory(ctx) {
68
106
  const archivePath = ".naome/archive";
69
107
  const targetPath = join(ctx.targetRoot, archivePath);
@@ -31,6 +31,8 @@ const legacyOptionalHookPaths = [
31
31
  "docs/naome/codex-hooks.md",
32
32
  ];
33
33
 
34
+ const trustedRetiredMachineOwnedPaths = new Set(legacyOptionalHookPaths);
35
+
34
36
  export async function runFreshInstall(ctx) {
35
37
  await confirmAgentsTakeover(ctx);
36
38
 
@@ -110,8 +112,11 @@ function removeRetiredMachineOwnedFiles(ctx, manifest, archiveDirName, options =
110
112
  ...ctx.localOnlyMachineOwnedPaths,
111
113
  ctx.nativeBinaryRelativePath,
112
114
  ].filter(Boolean));
115
+ const manifestRetiredPaths = Array.isArray(manifest?.machineOwned)
116
+ ? manifest.machineOwned.filter((path) => trustedRetiredMachineOwnedPaths.has(path))
117
+ : [];
113
118
  const retiredPaths = [
114
- ...(Array.isArray(manifest?.machineOwned) ? manifest.machineOwned : []),
119
+ ...manifestRetiredPaths,
115
120
  ...(Array.isArray(options.extraPaths) ? options.extraPaths : []),
116
121
  ];
117
122
 
@@ -9,8 +9,8 @@ import {
9
9
  } from "node:fs";
10
10
  import { dirname, join, relative } from "node:path";
11
11
 
12
- import { archiveUpgradePath, hasSymlinkInTargetPath } from "./filesystem.js";
13
- import { hasGeneratedIntegrity, machineFileHash } from "./native.js";
12
+ import { archiveUpgradePath, assertWritableArchivePath, hasSymlinkInTargetPath } from "./filesystem.js";
13
+ import { machineFileHash } from "./native.js";
14
14
  import { printError } from "./output.js";
15
15
 
16
16
  export function ensureTemplateFile(ctx, relativePath) {
@@ -55,8 +55,13 @@ export function removeLegacyHarnessFile(ctx, relativePath, archiveDirName) {
55
55
  return;
56
56
  }
57
57
 
58
- const archivePath = archiveUpgradePath(ctx, archiveDirName, relativePath);
58
+ const archivePath = safeArchivePath(ctx, archiveDirName, relativePath);
59
+ if (archivePath === null) {
60
+ failUnsafeArchivePath(ctx, archiveDirName, relativePath);
61
+ }
62
+
59
63
  mkdirSync(dirname(archivePath), { recursive: true });
64
+ assertWritableArchivePath(ctx, archiveDirName, relativePath);
60
65
  copyFileSync(targetPath, archivePath);
61
66
  unlinkSync(targetPath);
62
67
  ctx.updated.push(relativePath);
@@ -128,17 +133,40 @@ function replaceChangedHarnessFile(ctx, relativePath, archiveDirName, sourcePath
128
133
  return;
129
134
  }
130
135
 
131
- const archivePath = archiveUpgradePath(ctx, archiveDirName, relativePath);
136
+ const archivePath = safeArchivePath(ctx, archiveDirName, relativePath);
137
+ if (archivePath === null) {
138
+ failUnsafeArchivePath(ctx, archiveDirName, relativePath);
139
+ }
140
+
132
141
  mkdirSync(dirname(archivePath), { recursive: true });
142
+ assertWritableArchivePath(ctx, archiveDirName, relativePath);
133
143
  copyFileSync(targetPath, archivePath);
134
144
  writeFileSync(targetPath, nextContent);
135
145
  ctx.updated.push(relativePath);
136
146
  ctx.archived.push({ from: relativePath, to: relative(ctx.targetRoot, archivePath) });
137
147
  }
138
148
 
139
- function unchangedMachineHash(ctx, relativePath, currentContent, nextContent) {
140
- return (
141
- !hasGeneratedIntegrity(ctx, relativePath) &&
142
- machineFileHash(ctx, relativePath, currentContent) === machineFileHash(ctx, relativePath, nextContent)
149
+ function safeArchivePath(ctx, archiveDirName, relativePath) {
150
+ const archivePath = archiveUpgradePath(ctx, archiveDirName, relativePath);
151
+ const archiveParent = relative(ctx.targetRoot, dirname(archivePath));
152
+ if (hasSymlinkInTargetPath(ctx, archiveParent)) {
153
+ return null;
154
+ }
155
+ if (existsSync(archivePath)) {
156
+ return null;
157
+ }
158
+
159
+ return archivePath;
160
+ }
161
+
162
+ function failUnsafeArchivePath(ctx, archiveDirName, relativePath) {
163
+ printError(ctx, `NAOME cannot archive ${relativePath} safely.`);
164
+ console.error(
165
+ `${relative(ctx.targetRoot, archiveUpgradePath(ctx, archiveDirName, relativePath))} must not contain symlinks or pre-existing files.`
143
166
  );
167
+ process.exit(1);
168
+ }
169
+
170
+ function unchangedMachineHash(ctx, relativePath, currentContent, nextContent) {
171
+ return machineFileHash(ctx, relativePath, currentContent) === machineFileHash(ctx, relativePath, nextContent);
144
172
  }
@@ -22,6 +22,7 @@ export function ensureCoreHarnessFiles(ctx, archiveDirName) {
22
22
  replaceHarnessFile(ctx, "docs/naome/task-ledger.md", archiveDirName);
23
23
  ensureTemplateFile(ctx, "docs/naome/repository-model.md");
24
24
  ensureTemplateFile(ctx, "docs/naome/security.md");
25
+ ensureTemplateFile(ctx, "docs/naome/architecture-fitness.md");
25
26
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
26
27
  }
27
28
 
@@ -39,6 +40,7 @@ export function ensureTaskControlHarnessFiles(ctx, archiveDirName) {
39
40
  replaceHarnessFile(ctx, "docs/naome/agent-workflow.md", archiveDirName);
40
41
  replaceHarnessFile(ctx, "docs/naome/context-economy.md", archiveDirName);
41
42
  replaceHarnessFile(ctx, "docs/naome/execution.md", archiveDirName);
43
+ ensureTemplateFile(ctx, "docs/naome/architecture-fitness.md");
42
44
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
43
45
  }
44
46
 
@@ -57,6 +59,7 @@ export function ensureHarnessHealthFiles(ctx, archiveDirName) {
57
59
  replaceHarnessFile(ctx, "docs/naome/context-economy.md", archiveDirName);
58
60
  replaceHarnessFile(ctx, "docs/naome/execution.md", archiveDirName);
59
61
  ensureTemplateFile(ctx, "docs/naome/security.md");
62
+ ensureTemplateFile(ctx, "docs/naome/architecture-fitness.md");
60
63
  replaceHarnessFile(ctx, "docs/naome/upgrade.md", archiveDirName);
61
64
  ensureNaomeIgnore(ctx);
62
65
  }
@@ -2,7 +2,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "n
2
2
  import { dirname, join } from "node:path";
3
3
 
4
4
  import { hasSymlinkInTargetPath } from "./filesystem.js";
5
- import { installedNativeBinaryHash, templateIntegrity, usesSourceNativeFallback } from "./native.js";
5
+ import { installedMachineOwnedIntegrity, installedNativeBinaryHash, usesSourceNativeFallback } from "./native.js";
6
6
  import { printError } from "./output.js";
7
7
  import { isVersion } from "./version.js";
8
8
 
@@ -118,7 +118,7 @@ function applyManifestHealthMetadata(ctx, manifest) {
118
118
  manifest.harnessVersion = ctx.packageVersion;
119
119
  manifest.machineOwned = [...ctx.machineOwnedPaths];
120
120
  manifest.projectOwned = ctx.projectOwnedPaths;
121
- manifest.integrity = templateIntegrity(ctx);
121
+ manifest.integrity = installedMachineOwnedIntegrity(ctx);
122
122
 
123
123
  const nativeHash = installedNativeBinaryHash(ctx);
124
124
  if (!usesSourceNativeFallback(ctx) && nativeHash) {