@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,557 @@
1
+ use std::collections::{HashMap, HashSet};
2
+ use std::fs;
3
+ use std::path::{Component, Path};
4
+
5
+ use serde_json::Value;
6
+ use sha2::{Digest, Sha256};
7
+
8
+ use crate::models::NaomeError;
9
+
10
+ const HEALTH_CHECKER_RELATIVE_PATH: &str = ".naome/bin/check-harness-health.js";
11
+ const TASK_STATE_CHECKER_RELATIVE_PATH: &str = ".naome/bin/check-task-state.js";
12
+ const NAOME_COMMAND_RELATIVE_PATH: &str = ".naome/bin/naome.js";
13
+
14
+ #[cfg(windows)]
15
+ const NATIVE_BINARY_RELATIVE_PATH: &str = ".naome/bin/naome-rust.exe";
16
+ #[cfg(not(windows))]
17
+ const NATIVE_BINARY_RELATIVE_PATH: &str = ".naome/bin/naome-rust";
18
+
19
+ const EXPECTED_MACHINE_OWNED_PATHS: &[&str] = &[
20
+ "AGENTS.md",
21
+ ".naome/package.json",
22
+ ".naome/bin/naome.js",
23
+ ".naome/bin/check-task-state.js",
24
+ ".naome/bin/check-harness-health.js",
25
+ ".naome/task-contract.schema.json",
26
+ "docs/naome/index.md",
27
+ "docs/naome/first-run.md",
28
+ "docs/naome/agent-workflow.md",
29
+ "docs/naome/execution.md",
30
+ "docs/naome/upgrade.md",
31
+ ];
32
+
33
+ const EXPECTED_PROJECT_OWNED_PATHS: &[&str] = &[
34
+ ".naomeignore",
35
+ ".naome/init-state.json",
36
+ ".naome/manifest.json",
37
+ ".naome/task-state.json",
38
+ ".naome/upgrade-state.json",
39
+ ".naome/verification.json",
40
+ "docs/naome/architecture.md",
41
+ "docs/naome/decisions.md",
42
+ "docs/naome/repo-profile.md",
43
+ "docs/naome/security.md",
44
+ "docs/naome/testing.md",
45
+ ];
46
+
47
+ #[derive(Debug, Clone, Default)]
48
+ pub struct HarnessHealthOptions {
49
+ pub expected_integrity: HashMap<String, String>,
50
+ pub allow_missing_integrity: bool,
51
+ pub allow_missing_archive: bool,
52
+ }
53
+
54
+ pub fn validate_harness_health(
55
+ root: &Path,
56
+ options: HarnessHealthOptions,
57
+ ) -> Result<Vec<String>, NaomeError> {
58
+ let mut errors = Vec::new();
59
+
60
+ for relative_path in EXPECTED_MACHINE_OWNED_PATHS {
61
+ validate_regular_file(root, relative_path, &mut errors)?;
62
+ }
63
+
64
+ for relative_path in EXPECTED_PROJECT_OWNED_PATHS {
65
+ validate_regular_file(root, relative_path, &mut errors)?;
66
+ }
67
+
68
+ validate_directory(root, ".naome/archive", &mut errors, true)?;
69
+ validate_naome_ignore(root, &mut errors)?;
70
+ validate_agents_entrypoint(root, &mut errors)?;
71
+ validate_naome_package_scope(root, &mut errors)?;
72
+
73
+ if can_read_json(root, ".naome/manifest.json")? {
74
+ let manifest = read_json(root, ".naome/manifest.json", &mut errors)?;
75
+ if let Some(manifest) = manifest {
76
+ validate_manifest_shape(&manifest, &mut errors);
77
+ validate_manifest_ownership(&manifest, &mut errors);
78
+ validate_manifest_integrity(root, &manifest, &options, &mut errors)?;
79
+ validate_native_decision_binary(root, &manifest, &mut errors)?;
80
+ }
81
+ }
82
+
83
+ validate_json_object(root, ".naome/task-state.json", &mut errors)?;
84
+ validate_json_object(root, ".naome/upgrade-state.json", &mut errors)?;
85
+
86
+ Ok(errors)
87
+ }
88
+
89
+ fn validate_manifest_shape(manifest: &Value, errors: &mut Vec<String>) {
90
+ let Some(object) = manifest.as_object() else {
91
+ errors.push(".naome/manifest.json must be a JSON object.".to_string());
92
+ return;
93
+ };
94
+
95
+ if object.get("name").and_then(Value::as_str) != Some("naome") {
96
+ errors.push(".naome/manifest.json name must be naome.".to_string());
97
+ }
98
+
99
+ if !object
100
+ .get("harnessVersion")
101
+ .and_then(Value::as_str)
102
+ .is_some_and(is_version)
103
+ {
104
+ errors.push(".naome/manifest.json harnessVersion must be semver.".to_string());
105
+ }
106
+
107
+ if !string_array(object.get("machineOwned")).is_some() {
108
+ errors.push(".naome/manifest.json machineOwned must be a string array.".to_string());
109
+ }
110
+
111
+ if !string_array(object.get("projectOwned")).is_some() {
112
+ errors.push(".naome/manifest.json projectOwned must be a string array.".to_string());
113
+ }
114
+
115
+ if !object.get("integrity").is_some_and(Value::is_object) {
116
+ errors.push(".naome/manifest.json integrity must be an object.".to_string());
117
+ }
118
+ }
119
+
120
+ fn validate_manifest_ownership(manifest: &Value, errors: &mut Vec<String>) {
121
+ let Some(object) = manifest.as_object() else {
122
+ return;
123
+ };
124
+ let Some(machine_owned) = string_array(object.get("machineOwned")) else {
125
+ return;
126
+ };
127
+ let Some(project_owned) = string_array(object.get("projectOwned")) else {
128
+ return;
129
+ };
130
+
131
+ validate_contains_all(
132
+ &machine_owned,
133
+ EXPECTED_MACHINE_OWNED_PATHS,
134
+ ".naome/manifest.json machineOwned",
135
+ errors,
136
+ );
137
+ validate_contains_all(
138
+ &project_owned,
139
+ EXPECTED_PROJECT_OWNED_PATHS,
140
+ ".naome/manifest.json projectOwned",
141
+ errors,
142
+ );
143
+ }
144
+
145
+ fn validate_manifest_integrity(
146
+ root: &Path,
147
+ manifest: &Value,
148
+ options: &HarnessHealthOptions,
149
+ errors: &mut Vec<String>,
150
+ ) -> Result<(), NaomeError> {
151
+ let Some(integrity) = manifest.get("integrity").and_then(Value::as_object) else {
152
+ return Ok(());
153
+ };
154
+
155
+ for relative_path in EXPECTED_MACHINE_OWNED_PATHS {
156
+ let packaged_expected = options.expected_integrity.get(*relative_path);
157
+ let manifest_expected = integrity.get(*relative_path).and_then(Value::as_str);
158
+
159
+ let Some(packaged_expected) = packaged_expected else {
160
+ if options.allow_missing_integrity {
161
+ continue;
162
+ }
163
+
164
+ errors.push(format!("Packaged integrity missing {relative_path}."));
165
+ continue;
166
+ };
167
+
168
+ if !is_integrity_hash(packaged_expected) {
169
+ errors.push(format!("Packaged integrity missing {relative_path}."));
170
+ continue;
171
+ }
172
+
173
+ let Some(manifest_expected) = manifest_expected.filter(|value| is_integrity_hash(value))
174
+ else {
175
+ errors.push(format!(
176
+ ".naome/manifest.json integrity missing {relative_path}."
177
+ ));
178
+ continue;
179
+ };
180
+
181
+ if manifest_expected != packaged_expected {
182
+ errors.push(format!(
183
+ "{relative_path} manifest integrity does not match packaged integrity. Expected {packaged_expected}, got {manifest_expected}."
184
+ ));
185
+ continue;
186
+ }
187
+
188
+ if !root.join(relative_path).exists() || has_symlink_in_path(root, relative_path)? {
189
+ continue;
190
+ }
191
+
192
+ let content = fs::read(root.join(relative_path))?;
193
+ let actual = format!("sha256:{}", machine_sha256(relative_path, &content));
194
+ if actual != *packaged_expected {
195
+ errors.push(format!(
196
+ "{relative_path} integrity mismatch. Expected {packaged_expected}, got {actual}."
197
+ ));
198
+ }
199
+ }
200
+
201
+ Ok(())
202
+ }
203
+
204
+ fn validate_native_decision_binary(
205
+ root: &Path,
206
+ manifest: &Value,
207
+ errors: &mut Vec<String>,
208
+ ) -> Result<(), NaomeError> {
209
+ let Some(object) = manifest.as_object() else {
210
+ return Ok(());
211
+ };
212
+ let Some(machine_owned) = string_array(object.get("machineOwned")) else {
213
+ return Ok(());
214
+ };
215
+ let Some(integrity) = object.get("integrity").and_then(Value::as_object) else {
216
+ return Ok(());
217
+ };
218
+
219
+ if !machine_owned
220
+ .iter()
221
+ .any(|entry| entry == NATIVE_BINARY_RELATIVE_PATH)
222
+ {
223
+ return Ok(());
224
+ }
225
+
226
+ validate_regular_file(root, NATIVE_BINARY_RELATIVE_PATH, errors)?;
227
+
228
+ if !root.join(NATIVE_BINARY_RELATIVE_PATH).exists()
229
+ || has_symlink_in_path(root, NATIVE_BINARY_RELATIVE_PATH)?
230
+ {
231
+ return Ok(());
232
+ }
233
+
234
+ let Some(manifest_expected) = integrity
235
+ .get(NATIVE_BINARY_RELATIVE_PATH)
236
+ .and_then(Value::as_str)
237
+ .filter(|value| is_integrity_hash(value))
238
+ else {
239
+ errors.push(format!(
240
+ ".naome/manifest.json integrity missing {NATIVE_BINARY_RELATIVE_PATH}."
241
+ ));
242
+ return Ok(());
243
+ };
244
+
245
+ let actual = format!(
246
+ "sha256:{}",
247
+ sha256_bytes(&fs::read(root.join(NATIVE_BINARY_RELATIVE_PATH))?)
248
+ );
249
+ if actual != manifest_expected {
250
+ errors.push(format!(
251
+ "{NATIVE_BINARY_RELATIVE_PATH} integrity mismatch. Expected {manifest_expected}, got {actual}."
252
+ ));
253
+ }
254
+
255
+ let command_path = root.join(NAOME_COMMAND_RELATIVE_PATH);
256
+ if !command_path.exists() || has_symlink_in_path(root, NAOME_COMMAND_RELATIVE_PATH)? {
257
+ return Ok(());
258
+ }
259
+
260
+ let command_content = fs::read_to_string(command_path)?;
261
+ let command_expected = native_integrity_from_naome_command(&command_content);
262
+ if command_expected.as_deref() != Some(manifest_expected) {
263
+ errors.push(format!(
264
+ "{NAOME_COMMAND_RELATIVE_PATH} native binary integrity does not match .naome/manifest.json."
265
+ ));
266
+ }
267
+
268
+ Ok(())
269
+ }
270
+
271
+ fn validate_naome_ignore(root: &Path, errors: &mut Vec<String>) -> Result<(), NaomeError> {
272
+ let relative_path = ".naomeignore";
273
+ if !root.join(relative_path).exists() || has_symlink_in_path(root, relative_path)? {
274
+ return Ok(());
275
+ }
276
+
277
+ let content = fs::read_to_string(root.join(relative_path))?;
278
+ if !content
279
+ .lines()
280
+ .map(str::trim)
281
+ .any(|line| line == ".naome/archive/")
282
+ {
283
+ errors.push(".naomeignore must include .naome/archive/.".to_string());
284
+ }
285
+
286
+ Ok(())
287
+ }
288
+
289
+ fn validate_agents_entrypoint(root: &Path, errors: &mut Vec<String>) -> Result<(), NaomeError> {
290
+ let relative_path = "AGENTS.md";
291
+ if !root.join(relative_path).exists() || has_symlink_in_path(root, relative_path)? {
292
+ return Ok(());
293
+ }
294
+
295
+ let content = fs::read_to_string(root.join(relative_path))?;
296
+ if !content.contains("node .naome/bin/check-harness-health.js") {
297
+ errors.push(
298
+ "AGENTS.md must require node .naome/bin/check-harness-health.js before feature work."
299
+ .to_string(),
300
+ );
301
+ }
302
+
303
+ if !content.contains("node .naome/bin/check-task-state.js --admission") {
304
+ errors.push(
305
+ "AGENTS.md must require node .naome/bin/check-task-state.js --admission before feature work."
306
+ .to_string(),
307
+ );
308
+ }
309
+
310
+ Ok(())
311
+ }
312
+
313
+ fn validate_naome_package_scope(root: &Path, errors: &mut Vec<String>) -> Result<(), NaomeError> {
314
+ let relative_path = ".naome/package.json";
315
+ if !can_read_json(root, relative_path)? {
316
+ return Ok(());
317
+ }
318
+
319
+ let Some(value) = read_json(root, relative_path, errors)? else {
320
+ return Ok(());
321
+ };
322
+
323
+ if value.get("type").and_then(Value::as_str) != Some("commonjs") {
324
+ errors.push(".naome/package.json type must be commonjs.".to_string());
325
+ }
326
+
327
+ Ok(())
328
+ }
329
+
330
+ fn validate_json_object(
331
+ root: &Path,
332
+ relative_path: &str,
333
+ errors: &mut Vec<String>,
334
+ ) -> Result<(), NaomeError> {
335
+ if !can_read_json(root, relative_path)? {
336
+ return Ok(());
337
+ }
338
+
339
+ let Some(value) = read_json(root, relative_path, errors)? else {
340
+ return Ok(());
341
+ };
342
+
343
+ if !value.is_object() {
344
+ errors.push(format!("{relative_path} must be a JSON object."));
345
+ }
346
+
347
+ Ok(())
348
+ }
349
+
350
+ fn can_read_json(root: &Path, relative_path: &str) -> Result<bool, NaomeError> {
351
+ let path = root.join(relative_path);
352
+ Ok(path.exists() && !has_symlink_in_path(root, relative_path)? && path.is_file())
353
+ }
354
+
355
+ fn read_json(
356
+ root: &Path,
357
+ relative_path: &str,
358
+ errors: &mut Vec<String>,
359
+ ) -> Result<Option<Value>, NaomeError> {
360
+ let path = root.join(relative_path);
361
+ if !path.exists() {
362
+ errors.push(format!("{relative_path} is missing."));
363
+ return Ok(None);
364
+ }
365
+
366
+ match serde_json::from_str(&fs::read_to_string(path)?) {
367
+ Ok(value) => Ok(Some(value)),
368
+ Err(error) => {
369
+ errors.push(format!("{relative_path} is not valid JSON: {error}"));
370
+ Ok(None)
371
+ }
372
+ }
373
+ }
374
+
375
+ fn validate_contains_all(
376
+ actual_values: &[String],
377
+ expected_values: &[&str],
378
+ field_name: &str,
379
+ errors: &mut Vec<String>,
380
+ ) {
381
+ let actual: HashSet<&str> = actual_values.iter().map(String::as_str).collect();
382
+ for expected in expected_values {
383
+ if !actual.contains(expected) {
384
+ errors.push(format!("{field_name} must include {expected}."));
385
+ }
386
+ }
387
+ }
388
+
389
+ fn validate_regular_file(
390
+ root: &Path,
391
+ relative_path: &str,
392
+ errors: &mut Vec<String>,
393
+ ) -> Result<(), NaomeError> {
394
+ let path = root.join(relative_path);
395
+ if has_symlink_in_path(root, relative_path)? {
396
+ errors.push(format!("{relative_path} must not contain symlinks."));
397
+ return Ok(());
398
+ }
399
+
400
+ if !path.exists() {
401
+ errors.push(format!("{relative_path} is missing."));
402
+ return Ok(());
403
+ }
404
+
405
+ if !path.is_file() {
406
+ errors.push(format!("{relative_path} must be a regular file."));
407
+ }
408
+
409
+ Ok(())
410
+ }
411
+
412
+ fn validate_directory(
413
+ root: &Path,
414
+ relative_path: &str,
415
+ errors: &mut Vec<String>,
416
+ allow_missing: bool,
417
+ ) -> Result<(), NaomeError> {
418
+ let path = root.join(relative_path);
419
+ if has_symlink_in_path(root, relative_path)? {
420
+ errors.push(format!("{relative_path} must not contain symlinks."));
421
+ return Ok(());
422
+ }
423
+
424
+ if !path.exists() {
425
+ if !allow_missing {
426
+ errors.push(format!("{relative_path} is missing."));
427
+ }
428
+ return Ok(());
429
+ }
430
+
431
+ if !path.is_dir() {
432
+ errors.push(format!("{relative_path} must be a directory."));
433
+ }
434
+
435
+ Ok(())
436
+ }
437
+
438
+ fn has_symlink_in_path(root: &Path, relative_path: &str) -> Result<bool, NaomeError> {
439
+ let mut current = root.to_path_buf();
440
+ for component in Path::new(relative_path).components() {
441
+ match component {
442
+ Component::Normal(part) => current.push(part),
443
+ _ => continue,
444
+ }
445
+
446
+ match fs::symlink_metadata(&current) {
447
+ Ok(metadata) if metadata.file_type().is_symlink() => return Ok(true),
448
+ Ok(_) => {}
449
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
450
+ Err(error) => return Err(error.into()),
451
+ }
452
+ }
453
+
454
+ Ok(false)
455
+ }
456
+
457
+ fn string_array(value: Option<&Value>) -> Option<Vec<String>> {
458
+ value.and_then(Value::as_array).and_then(|entries| {
459
+ entries
460
+ .iter()
461
+ .map(|entry| {
462
+ entry
463
+ .as_str()
464
+ .filter(|text| !text.trim().is_empty())
465
+ .map(ToString::to_string)
466
+ })
467
+ .collect()
468
+ })
469
+ }
470
+
471
+ fn is_version(value: &str) -> bool {
472
+ let parts: Vec<&str> = value.split('.').collect();
473
+ parts.len() == 3
474
+ && parts
475
+ .iter()
476
+ .all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
477
+ }
478
+
479
+ fn is_integrity_hash(value: &str) -> bool {
480
+ value.len() == "sha256:".len() + 64
481
+ && value.starts_with("sha256:")
482
+ && value["sha256:".len()..]
483
+ .chars()
484
+ .all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase())
485
+ }
486
+
487
+ fn machine_sha256(relative_path: &str, content: &[u8]) -> String {
488
+ let normalized = normalize_machine_owned_content(relative_path, content);
489
+ sha256_bytes(&normalized)
490
+ }
491
+
492
+ fn sha256_bytes(content: &[u8]) -> String {
493
+ let mut hasher = Sha256::new();
494
+ hasher.update(content);
495
+ format!("{:x}", hasher.finalize())
496
+ }
497
+
498
+ fn normalize_machine_owned_content(relative_path: &str, content: &[u8]) -> Vec<u8> {
499
+ if relative_path == HEALTH_CHECKER_RELATIVE_PATH || relative_path == TASK_STATE_CHECKER_RELATIVE_PATH {
500
+ return replace_expected_integrity_block(content);
501
+ }
502
+
503
+ if relative_path == NAOME_COMMAND_RELATIVE_PATH {
504
+ return replace_native_integrity_line(content);
505
+ }
506
+
507
+ content.to_vec()
508
+ }
509
+
510
+ fn replace_expected_integrity_block(content: &[u8]) -> Vec<u8> {
511
+ let text = String::from_utf8_lossy(content);
512
+ let start_marker = "const expectedMachineOwnedIntegrity = Object.freeze({\n";
513
+ let normalized =
514
+ "const expectedMachineOwnedIntegrity = Object.freeze({\n __generated__: \"sha256:generated\"\n});\n";
515
+ let Some(start) = text.find(start_marker) else {
516
+ return content.to_vec();
517
+ };
518
+ let after_start = start + start_marker.len();
519
+ let Some(relative_end) = text[after_start..].find("\n});\n") else {
520
+ return content.to_vec();
521
+ };
522
+ let end = after_start + relative_end + "\n});\n".len();
523
+
524
+ let mut next = String::with_capacity(text.len());
525
+ next.push_str(&text[..start]);
526
+ next.push_str(normalized);
527
+ next.push_str(&text[end..]);
528
+ next.into_bytes()
529
+ }
530
+
531
+ fn replace_native_integrity_line(content: &[u8]) -> Vec<u8> {
532
+ let text = String::from_utf8_lossy(content);
533
+ let prefix = "const expectedNativeBinaryIntegrity = \"";
534
+ let Some(start) = text.find(prefix) else {
535
+ return content.to_vec();
536
+ };
537
+ let Some(relative_end) = text[start..].find(";\n") else {
538
+ return content.to_vec();
539
+ };
540
+ let end = start + relative_end + ";\n".len();
541
+ let replacement = "const expectedNativeBinaryIntegrity = \"sha256:generated\";\n";
542
+
543
+ let mut next = String::with_capacity(text.len());
544
+ next.push_str(&text[..start]);
545
+ next.push_str(replacement);
546
+ next.push_str(&text[end..]);
547
+ next.into_bytes()
548
+ }
549
+
550
+ fn native_integrity_from_naome_command(content: &str) -> Option<String> {
551
+ let prefix = "const expectedNativeBinaryIntegrity = \"";
552
+ let start = content.find(prefix)? + prefix.len();
553
+ let rest = &content[start..];
554
+ let end = rest.find("\";")?;
555
+ let value = &rest[..end];
556
+ is_integrity_hash(value).then(|| value.to_string())
557
+ }
@@ -0,0 +1,82 @@
1
+ use serde::Serialize;
2
+
3
+ pub const MACHINE_OWNED_PATHS: &[&str] = &[
4
+ "AGENTS.md",
5
+ ".naome/package.json",
6
+ ".naome/bin/naome.js",
7
+ ".naome/bin/check-task-state.js",
8
+ ".naome/bin/check-harness-health.js",
9
+ ".naome/task-contract.schema.json",
10
+ "docs/naome/index.md",
11
+ "docs/naome/first-run.md",
12
+ "docs/naome/agent-workflow.md",
13
+ "docs/naome/execution.md",
14
+ "docs/naome/upgrade.md",
15
+ ];
16
+
17
+ pub const PROJECT_OWNED_PATHS: &[&str] = &[
18
+ ".naomeignore",
19
+ ".naome/init-state.json",
20
+ ".naome/manifest.json",
21
+ ".naome/task-state.json",
22
+ ".naome/upgrade-state.json",
23
+ ".naome/verification.json",
24
+ "docs/naome/architecture.md",
25
+ "docs/naome/decisions.md",
26
+ "docs/naome/repo-profile.md",
27
+ "docs/naome/security.md",
28
+ "docs/naome/testing.md",
29
+ ];
30
+
31
+ pub const LOCAL_ONLY_MACHINE_OWNED_PATHS: &[&str] = &[
32
+ ".naome/package.json",
33
+ ".naome/bin/naome.js",
34
+ ".naome/bin/check-task-state.js",
35
+ ".naome/bin/check-harness-health.js",
36
+ ".naome/task-contract.schema.json",
37
+ "docs/naome/index.md",
38
+ "docs/naome/first-run.md",
39
+ "docs/naome/agent-workflow.md",
40
+ "docs/naome/execution.md",
41
+ "docs/naome/upgrade.md",
42
+ ];
43
+
44
+ const LOCAL_NATIVE_BINARY_PATHS: &[&str] = &[
45
+ ".naome/bin/naome-rust",
46
+ ".naome/bin/naome-rust.exe",
47
+ ".naome/archive",
48
+ ];
49
+
50
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
51
+ #[serde(rename_all = "camelCase")]
52
+ pub struct InstallPlan {
53
+ pub schema: &'static str,
54
+ pub harness_version: String,
55
+ pub machine_owned: Vec<&'static str>,
56
+ pub project_owned: Vec<&'static str>,
57
+ pub local_only_machine_owned: Vec<&'static str>,
58
+ pub gitignore_entries: Vec<&'static str>,
59
+ pub git_untrack_paths: Vec<&'static str>,
60
+ }
61
+
62
+ pub fn install_plan(harness_version: impl Into<String>) -> InstallPlan {
63
+ let mut gitignore_entries = vec![
64
+ "# NAOME local machine-owned harness files.",
65
+ ".naome/archive/",
66
+ ".naome/bin/naome-rust*",
67
+ ];
68
+ gitignore_entries.extend_from_slice(LOCAL_ONLY_MACHINE_OWNED_PATHS);
69
+
70
+ let mut git_untrack_paths = LOCAL_ONLY_MACHINE_OWNED_PATHS.to_vec();
71
+ git_untrack_paths.extend_from_slice(LOCAL_NATIVE_BINARY_PATHS);
72
+
73
+ InstallPlan {
74
+ schema: "naome.install-plan.v1",
75
+ harness_version: harness_version.into(),
76
+ machine_owned: MACHINE_OWNED_PATHS.to_vec(),
77
+ project_owned: PROJECT_OWNED_PATHS.to_vec(),
78
+ local_only_machine_owned: LOCAL_ONLY_MACHINE_OWNED_PATHS.to_vec(),
79
+ gitignore_entries,
80
+ git_untrack_paths,
81
+ }
82
+ }
@@ -0,0 +1,17 @@
1
+ mod decision;
2
+ mod git;
3
+ mod harness_health;
4
+ mod install_plan;
5
+ mod models;
6
+ mod paths;
7
+ mod task_state;
8
+ mod verification;
9
+ mod verification_contract;
10
+
11
+ pub use decision::{evaluate_decision, format_decision, EvaluationOptions};
12
+ pub use harness_health::{validate_harness_health, HarnessHealthOptions};
13
+ pub use install_plan::{install_plan, InstallPlan};
14
+ pub use models::{Decision, NaomeError};
15
+ pub use task_state::{validate_task_state, TaskStateMode, TaskStateOptions, TaskStateReport};
16
+ pub use verification::seed_builtin_verification_checks;
17
+ pub use verification_contract::validate_verification_contract;