@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.
Files changed (48) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +5 -0
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-cli/src/install_bridge.rs +56 -8
  5. package/crates/naome-core/Cargo.toml +1 -1
  6. package/crates/naome-core/src/context/select.rs +58 -4
  7. package/crates/naome-core/src/harness_health/integrity.rs +41 -23
  8. package/crates/naome-core/src/harness_health/manifest.rs +97 -0
  9. package/crates/naome-core/src/harness_health.rs +58 -106
  10. package/crates/naome-core/src/intent/classifier.rs +56 -81
  11. package/crates/naome-core/src/intent/envelope.rs +173 -19
  12. package/crates/naome-core/src/intent/legacy_response.rs +2 -0
  13. package/crates/naome-core/src/intent/model.rs +6 -0
  14. package/crates/naome-core/src/intent/resolver.rs +25 -0
  15. package/crates/naome-core/src/intent/risk.rs +11 -1
  16. package/crates/naome-core/src/intent.rs +1 -1
  17. package/crates/naome-core/src/quality/cache.rs +122 -19
  18. package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
  19. package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
  20. package/crates/naome-core/src/quality/scanner.rs +5 -2
  21. package/crates/naome-core/src/route/context.rs +8 -0
  22. package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
  23. package/crates/naome-core/tests/context.rs +92 -0
  24. package/crates/naome-core/tests/harness_health.rs +149 -0
  25. package/crates/naome-core/tests/intent.rs +98 -18
  26. package/crates/naome-core/tests/intent_support/mod.rs +39 -1
  27. package/crates/naome-core/tests/intent_v2.rs +299 -10
  28. package/crates/naome-core/tests/quality_performance.rs +63 -2
  29. package/crates/naome-core/tests/repo_support/routes.rs +8 -2
  30. package/crates/naome-core/tests/route_baseline.rs +29 -0
  31. package/crates/naome-core/tests/route_completion.rs +26 -5
  32. package/crates/naome-core/tests/route_harness_refresh.rs +7 -1
  33. package/crates/naome-core/tests/route_user_diff.rs +1 -1
  34. package/crates/naome-core/tests/task_state_compact.rs +7 -1
  35. package/installer/filesystem.js +38 -0
  36. package/installer/flows.js +6 -1
  37. package/installer/harness-file-ops.js +36 -8
  38. package/installer/manifest-state.js +2 -2
  39. package/installer/native.js +63 -18
  40. package/native/darwin-arm64/naome +0 -0
  41. package/native/linux-x64/naome +0 -0
  42. package/package.json +1 -1
  43. package/templates/naome-root/.naome/bin/check-harness-health.js +25 -21
  44. package/templates/naome-root/.naome/bin/check-task-state.js +35 -42
  45. package/templates/naome-root/.naome/manifest.json +10 -10
  46. package/templates/naome-root/docs/naome/agent-workflow.md +14 -5
  47. package/templates/naome-root/docs/naome/architecture.md +9 -0
  48. package/crates/naome-core/src/intent/patterns.rs +0 -170
@@ -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");
@@ -6,10 +6,16 @@ use super::TestRepo;
6
6
 
7
7
  const NEW_README_TASK_PROMPT: &str = "Add another line to README as a new task.";
8
8
 
9
+ pub fn prompt_env(prompt: &str, workflow: &str, task: &str, mutation_intent: &str) -> String {
10
+ format!(
11
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"{mutation_intent}\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
12
+ )
13
+ }
14
+
9
15
  pub fn route_commit_request(repo: &TestRepo) -> RouteDecision {
10
16
  evaluate_route(
11
17
  repo.path(),
12
- "commit my changes",
18
+ &prompt_env("commit my changes", "commit_request", "none", "commit"),
13
19
  RouteOptions {
14
20
  execute: true,
15
21
  evaluation: EvaluationOptions::offline(),
@@ -29,7 +35,7 @@ pub fn try_route_new_task(
29
35
  ) -> Result<RouteDecision, NaomeError> {
30
36
  evaluate_route(
31
37
  repo.path(),
32
- prompt,
38
+ &prompt_env(prompt, "none", "new_task", "modify_files"),
33
39
  RouteOptions {
34
40
  execute,
35
41
  evaluation: EvaluationOptions::offline(),
@@ -3,11 +3,40 @@ use std::fs;
3
3
 
4
4
  mod repo_support;
5
5
 
6
+ use naome_core::{evaluate_route, EvaluationOptions, RouteOptions};
6
7
  use repo_support::{
7
8
  assert_commit_paths, assert_isolated_worktree_ready, route_new_task, route_readme_task,
8
9
  TestRepo,
9
10
  };
10
11
 
12
+ #[test]
13
+ fn execute_route_with_raw_prompt_requests_normalization_without_mutating() {
14
+ let repo = TestRepo::new("route-raw-prompt-normalize-first");
15
+ repo.init_git();
16
+ repo.write_file("README.md", "# Baseline\n");
17
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
18
+ repo.git(&["add", "."]);
19
+ repo.git(&["commit", "-m", "baseline"]);
20
+ let before = repo.git_stdout(&["rev-parse", "HEAD"]);
21
+
22
+ let route = evaluate_route(
23
+ repo.path(),
24
+ "Please implement, commit, push, and create the MR.",
25
+ RouteOptions {
26
+ execute: true,
27
+ evaluation: EvaluationOptions::offline(),
28
+ },
29
+ )
30
+ .unwrap();
31
+
32
+ assert_eq!(route.prompt_intent, "prompt_normalization_required");
33
+ assert_eq!(route.policy_action, "normalize_prompt_first");
34
+ assert!(!route.mutation_performed);
35
+ assert!(!route.can_create_task);
36
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
37
+ assert!(repo.git_status_short().is_empty());
38
+ }
39
+
11
40
  #[test]
12
41
  fn dry_route_reports_auto_baseline_without_mutating() {
13
42
  let repo = TestRepo::completed_task_with_diff("route-dry-auto");
@@ -7,6 +7,12 @@ mod repo_support;
7
7
 
8
8
  use repo_support::TestRepo;
9
9
 
10
+ fn route_prompt(prompt: &str, workflow: &str, task: &str, mutation_intent: &str) -> String {
11
+ format!(
12
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"{mutation_intent}\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
13
+ )
14
+ }
15
+
10
16
  #[test]
11
17
  fn execute_route_does_not_mutate_when_prompt_blocks_commit() {
12
18
  let repo = TestRepo::completed_task_with_diff("route-no-commit");
@@ -14,7 +20,12 @@ fn execute_route_does_not_mutate_when_prompt_blocks_commit() {
14
20
 
15
21
  let route = evaluate_route(
16
22
  repo.path(),
17
- "Do not commit. Start a new task after this.",
23
+ &route_prompt(
24
+ "Do not commit. Start a new task after this.",
25
+ "no_commit_request",
26
+ "new_task",
27
+ "none",
28
+ ),
18
29
  RouteOptions {
19
30
  execute: true,
20
31
  evaluation: EvaluationOptions::offline(),
@@ -41,7 +52,7 @@ fn explicit_route_commit_baseline_leaves_unrelated_user_edit_unstaged() {
41
52
 
42
53
  let route = evaluate_route(
43
54
  repo.path(),
44
- "commit_task_baseline",
55
+ &route_prompt("commit_task_baseline", "commit_request", "none", "commit"),
45
56
  RouteOptions {
46
57
  execute: true,
47
58
  evaluation: EvaluationOptions::offline(),
@@ -67,7 +78,12 @@ fn execute_route_journals_external_commit_after_completed_task() {
67
78
 
68
79
  let route = evaluate_route(
69
80
  repo.path(),
70
- "Create a new task for README polish.",
81
+ &route_prompt(
82
+ "Create a new task for README polish.",
83
+ "none",
84
+ "new_task",
85
+ "modify_files",
86
+ ),
71
87
  RouteOptions {
72
88
  execute: true,
73
89
  evaluation: EvaluationOptions::offline(),
@@ -94,7 +110,12 @@ fn explain_reports_winning_rule_and_mutation_plan_without_executing() {
94
110
 
95
111
  let explain = explain_route(
96
112
  repo.path(),
97
- "Start a new task for README polish.",
113
+ &route_prompt(
114
+ "Start a new task for README polish.",
115
+ "none",
116
+ "new_task",
117
+ "modify_files",
118
+ ),
98
119
  EvaluationOptions::offline(),
99
120
  )
100
121
  .unwrap();
@@ -126,7 +147,7 @@ fn unhealthy_harness_route_blocks_normal_work() {
126
147
 
127
148
  let route = evaluate_route(
128
149
  repo.path(),
129
- "Create a new task.",
150
+ &route_prompt("Create a new task.", "none", "new_task", "modify_files"),
130
151
  RouteOptions {
131
152
  execute: true,
132
153
  evaluation: EvaluationOptions::online(),
@@ -9,6 +9,12 @@ use repo_support::{
9
9
  TestRepo,
10
10
  };
11
11
 
12
+ fn repair_prompt(prompt: &str) -> String {
13
+ format!(
14
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"fix\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"repair_request\",\"taskIntent\":\"none\",\"risk\":\"none\",\"requestedActions\":[\"repair\"],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
15
+ )
16
+ }
17
+
12
18
  #[test]
13
19
  fn execute_route_baselines_harness_refresh_before_dirty_repo_worktree() {
14
20
  let repo = TestRepo::dirty_harness_refresh_repo("route-dirty-harness-refresh-worktree", true);
@@ -66,7 +72,7 @@ fn execute_route_repair_request_baselines_harness_refresh_only() {
66
72
 
67
73
  let route = evaluate_route(
68
74
  repo.path(),
69
- "please repair all",
75
+ &repair_prompt("please repair all"),
70
76
  RouteOptions {
71
77
  execute: true,
72
78
  evaluation: EvaluationOptions::offline(),
@@ -27,7 +27,7 @@ fn execute_route_does_not_mutate_or_offer_clear_commit_for_dirty_unowned_diff()
27
27
  )
28
28
  .unwrap();
29
29
 
30
- assert_eq!(route.policy_action, "block_unowned_diff");
30
+ assert_eq!(route.policy_action, "normalize_prompt_first");
31
31
  assert!(!route.mutation_performed);
32
32
  assert!(!route.can_create_task);
33
33
  assert_eq!(route.executed_actions, Vec::<String>::new());
@@ -6,6 +6,12 @@ use task_state_compact_support::{
6
6
  compact_state, large_compact_state, large_expanded_state, legacy_state, MiniRepo,
7
7
  };
8
8
 
9
+ fn new_task_prompt(prompt: &str) -> String {
10
+ format!(
11
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
12
+ )
13
+ }
14
+
9
15
  #[test]
10
16
  fn legacy_v1_proof_results_remain_valid() {
11
17
  let repo = MiniRepo::new();
@@ -30,7 +36,7 @@ fn compact_path_sets_and_batches_cover_completion_commit_and_route() {
30
36
 
31
37
  let route = evaluate_route(
32
38
  repo.path(),
33
- "new task",
39
+ &new_task_prompt("new task"),
34
40
  RouteOptions {
35
41
  execute: false,
36
42
  evaluation: EvaluationOptions::offline(),
@@ -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
  }
@@ -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) {
@@ -6,6 +6,7 @@ import {
6
6
  lstatSync,
7
7
  mkdirSync,
8
8
  readFileSync,
9
+ unlinkSync,
9
10
  writeFileSync,
10
11
  } from "node:fs";
11
12
  import { dirname, join, resolve } from "node:path";
@@ -32,8 +33,8 @@ export function installNativeDecisionBinary(ctx) {
32
33
  }
33
34
 
34
35
  if (usesSourceNativeFallback(ctx)) {
35
- patchNaomeCommandNativeIntegrity(ctx, "sha256:generated");
36
- ctx.skipped.push(ctx.nativeBinaryRelativePath);
36
+ removeInstalledNativeDecisionBinary(ctx);
37
+ patchInstalledNativeIntegrity(ctx, "sha256:generated");
37
38
  return;
38
39
  }
39
40
 
@@ -52,7 +53,7 @@ export function installNativeDecisionBinary(ctx) {
52
53
  }
53
54
 
54
55
  chmodSync(targetPath, 0o755);
55
- patchNaomeCommandNativeIntegrity(ctx, `sha256:${sourceHash}`);
56
+ patchInstalledNativeIntegrity(ctx, `sha256:${sourceHash}`);
56
57
  }
57
58
 
58
59
  export function findNativeDecisionBinary(ctx) {
@@ -75,7 +76,7 @@ export function findNativeDecisionBinary(ctx) {
75
76
  }
76
77
 
77
78
  export function patchInstalledMachineOwnedIntegrity(ctx) {
78
- const integrityBlock = formatExpectedIntegrityBlock(templateIntegrity(ctx));
79
+ const integrityBlock = formatExpectedIntegrityBlock(installedMachineOwnedIntegrity(ctx));
79
80
 
80
81
  for (const relativePath of [ctx.healthCheckerRelativePath, ctx.taskStateCheckerRelativePath]) {
81
82
  const targetPath = join(ctx.targetRoot, relativePath);
@@ -110,6 +111,23 @@ export function installedNativeBinaryHash(ctx) {
110
111
  return sha256(readFileSync(targetPath));
111
112
  }
112
113
 
114
+ function removeInstalledNativeDecisionBinary(ctx) {
115
+ const targetPath = join(ctx.targetRoot, ctx.nativeBinaryRelativePath);
116
+ if (!existsSync(targetPath)) {
117
+ ctx.skipped.push(ctx.nativeBinaryRelativePath);
118
+ return;
119
+ }
120
+
121
+ if (hasSymlinkInTargetPath(ctx, ctx.nativeBinaryRelativePath) || !lstatSync(targetPath).isFile()) {
122
+ ctx.skipped.push(ctx.nativeBinaryRelativePath);
123
+ ctx.unsafeSkipped.push(ctx.nativeBinaryRelativePath);
124
+ return;
125
+ }
126
+
127
+ unlinkSync(targetPath);
128
+ ctx.updated.push(ctx.nativeBinaryRelativePath);
129
+ }
130
+
113
131
  export function templateIntegrity(ctx) {
114
132
  const integrity = {};
115
133
 
@@ -121,6 +139,17 @@ export function templateIntegrity(ctx) {
121
139
  return integrity;
122
140
  }
123
141
 
142
+ export function installedMachineOwnedIntegrity(ctx) {
143
+ const integrity = templateIntegrity(ctx);
144
+ const nativeHash = installedNativeBinaryHash(ctx);
145
+
146
+ if (!usesSourceNativeFallback(ctx) && nativeHash) {
147
+ integrity[ctx.nativeBinaryRelativePath] = `sha256:${nativeHash}`;
148
+ }
149
+
150
+ return integrity;
151
+ }
152
+
124
153
  export function sha256(content) {
125
154
  return createHash("sha256").update(content).digest("hex");
126
155
  }
@@ -132,7 +161,7 @@ export function machineFileHash(ctx, relativePath, content) {
132
161
  normalized = normalized.toString("utf8").replace(ctx.integrityBlockPattern, ctx.normalizedIntegrityBlock);
133
162
  }
134
163
 
135
- if (relativePath === ctx.naomeCommandRelativePath) {
164
+ if (hasGeneratedNativeIntegrity(ctx, relativePath)) {
136
165
  normalized = normalized.toString("utf8").replace(ctx.nativeIntegrityPattern, ctx.normalizedNativeIntegrity);
137
166
  }
138
167
 
@@ -143,20 +172,36 @@ export function hasGeneratedIntegrity(ctx, relativePath) {
143
172
  return relativePath === ctx.healthCheckerRelativePath || relativePath === ctx.taskStateCheckerRelativePath;
144
173
  }
145
174
 
146
- function patchNaomeCommandNativeIntegrity(ctx, expectedIntegrity) {
147
- const commandPath = join(ctx.targetRoot, ctx.naomeCommandRelativePath);
148
- if (!existsSync(commandPath) || hasSymlinkInTargetPath(ctx, ctx.naomeCommandRelativePath)) {
149
- return;
150
- }
175
+ export function hasGeneratedNativeIntegrity(ctx, relativePath) {
176
+ return [
177
+ ctx.healthCheckerRelativePath,
178
+ ctx.taskStateCheckerRelativePath,
179
+ ctx.naomeCommandRelativePath,
180
+ ].includes(relativePath);
181
+ }
182
+
183
+ function patchInstalledNativeIntegrity(ctx, expectedIntegrity) {
184
+ const nativeIntegrityPaths = [
185
+ ctx.healthCheckerRelativePath,
186
+ ctx.taskStateCheckerRelativePath,
187
+ ctx.naomeCommandRelativePath,
188
+ ];
189
+
190
+ for (const relativePath of nativeIntegrityPaths) {
191
+ const targetPath = join(ctx.targetRoot, relativePath);
192
+ if (!existsSync(targetPath) || hasSymlinkInTargetPath(ctx, relativePath)) {
193
+ continue;
194
+ }
151
195
 
152
- const content = readFileSync(commandPath, "utf8");
153
- const nextContent = content.replace(
154
- ctx.nativeIntegrityPattern,
155
- `const expectedNativeBinaryIntegrity = "${expectedIntegrity}";\n`,
156
- );
196
+ const content = readFileSync(targetPath, "utf8");
197
+ const nextContent = content.replace(
198
+ ctx.nativeIntegrityPattern,
199
+ `const expectedNativeBinaryIntegrity = "${expectedIntegrity}";\n`,
200
+ );
157
201
 
158
- if (nextContent !== content) {
159
- writeFileSync(commandPath, nextContent);
160
- ctx.updated.push(ctx.naomeCommandRelativePath);
202
+ if (nextContent !== content) {
203
+ writeFileSync(targetPath, nextContent);
204
+ ctx.updated.push(relativePath);
205
+ }
161
206
  }
162
207
  }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",