@lamentis/naome 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.1.0"
79
+ version = "1.1.1"
80
80
  dependencies = [
81
81
  "naome-core",
82
82
  "serde_json",
@@ -84,7 +84,7 @@ dependencies = [
84
84
 
85
85
  [[package]]
86
86
  name = "naome-core"
87
- version = "1.1.0"
87
+ version = "1.1.1"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
package/bin/naome-node.js CHANGED
@@ -513,6 +513,7 @@ async function runFreshInstall() {
513
513
  patchInstalledMachineOwnedIntegrity();
514
514
  ensureBuiltInVerificationChecks();
515
515
  patchManifestDate();
516
+ ensureCompleteUpgradeState(null);
516
517
  ensureArchiveDirectory();
517
518
  takeoverExistingAgents();
518
519
  ensureLocalOnlySourceControlBoundary();
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.1.0"
3
+ version = "1.1.1"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.1.0"
3
+ version = "1.1.1"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -109,13 +109,19 @@ pub fn evaluate_route(
109
109
  .to_string();
110
110
  }
111
111
  "auto_commit_completed_task_then_create_isolated_task_worktree" => {
112
- let before = git_head(root)?;
112
+ let worktree_name_head = task_worktree_name_head(root)?;
113
+ preflight_isolated_task_worktree(root, prompt, &worktree_name_head)?;
114
+ let before = Some(worktree_name_head.clone());
113
115
  git_add_completed_task_paths(root)?;
114
116
  git_commit(root, "chore(naome): baseline completed task")?;
115
117
  let after = git_head(root)?;
116
118
  journal_entry =
117
119
  append_task_journal(root, "route_auto_baseline", before, after.clone())?;
118
- let created = create_isolated_task_worktree(root, prompt)?;
120
+ let created = create_isolated_task_worktree_with_name_head(
121
+ root,
122
+ prompt,
123
+ &worktree_name_head,
124
+ )?;
119
125
  task_root = PathBuf::from(&created.path);
120
126
  mutation_performed = true;
121
127
  executed_actions.push("commit_task_baseline".to_string());
@@ -147,6 +153,8 @@ pub fn evaluate_route(
147
153
  user_message = "NAOME baselined the harness refresh and completed task, then admitted the next task.".to_string();
148
154
  }
149
155
  "auto_commit_harness_refresh_then_create_isolated_task_worktree" => {
156
+ let worktree_name_head = task_worktree_name_head(root)?;
157
+ preflight_isolated_task_worktree(root, prompt, &worktree_name_head)?;
150
158
  let Some(split) = harness_refresh_with_unrelated_diff(root)? else {
151
159
  return Err(NaomeError::new(
152
160
  "Unable to split harness refresh paths from unrelated dirty paths.",
@@ -154,7 +162,11 @@ pub fn evaluate_route(
154
162
  };
155
163
  git_stage_only_paths(root, &split.harness_paths)?;
156
164
  git_commit(root, "chore(naome): baseline harness refresh")?;
157
- let created = create_isolated_task_worktree(root, prompt)?;
165
+ let created = create_isolated_task_worktree_with_name_head(
166
+ root,
167
+ prompt,
168
+ &worktree_name_head,
169
+ )?;
158
170
  task_root = PathBuf::from(&created.path);
159
171
  mutation_performed = true;
160
172
  executed_actions.push("commit_harness_refresh_baseline".to_string());
@@ -848,10 +860,19 @@ fn git_commit(root: &Path, message: &str) -> Result<(), NaomeError> {
848
860
  }
849
861
 
850
862
  fn create_isolated_task_worktree(root: &Path, prompt: &str) -> Result<RouteWorktree, NaomeError> {
863
+ let name_head = task_worktree_name_head(root)?;
864
+ create_isolated_task_worktree_with_name_head(root, prompt, &name_head)
865
+ }
866
+
867
+ fn create_isolated_task_worktree_with_name_head(
868
+ root: &Path,
869
+ prompt: &str,
870
+ name_head: &str,
871
+ ) -> Result<RouteWorktree, NaomeError> {
851
872
  let base_head = git_head(root)?.ok_or_else(|| {
852
873
  NaomeError::new("Cannot create task worktree because the repository has no HEAD.")
853
874
  })?;
854
- let short_head = base_head.chars().take(12).collect::<String>();
875
+ let short_head = name_head.chars().take(12).collect::<String>();
855
876
  let slug = prompt_slug(prompt);
856
877
  let common_git_dir = git_common_dir(root)?;
857
878
  let worktree_base = common_git_dir.join("naome").join("worktrees");
@@ -900,6 +921,48 @@ fn create_isolated_task_worktree(root: &Path, prompt: &str) -> Result<RouteWorkt
900
921
  ))
901
922
  }
902
923
 
924
+ fn preflight_isolated_task_worktree(
925
+ root: &Path,
926
+ prompt: &str,
927
+ name_head: &str,
928
+ ) -> Result<(), NaomeError> {
929
+ let short_head = name_head.chars().take(12).collect::<String>();
930
+ let slug = prompt_slug(prompt);
931
+ let common_git_dir = git_common_dir(root)?;
932
+ let worktree_base = common_git_dir.join("naome").join("worktrees");
933
+ fs::create_dir_all(&worktree_base)?;
934
+ let worktree_count = existing_naome_task_worktree_count(&worktree_base)?;
935
+ if worktree_count >= MAX_NAOME_TASK_WORKTREES {
936
+ return Err(NaomeError::new(format!(
937
+ "Too many NAOME task worktrees are present ({worktree_count}). Finish or remove old task worktrees before creating another isolated task worktree."
938
+ )));
939
+ }
940
+
941
+ for attempt in 1..100 {
942
+ let suffix = if attempt == 1 {
943
+ String::new()
944
+ } else {
945
+ format!("-{attempt}")
946
+ };
947
+ let name = format!("{slug}-{short_head}{suffix}");
948
+ let branch = format!("naome/task/{name}");
949
+ let path = worktree_base.join(&name);
950
+ if !path.exists() && !git_branch_exists(root, &branch)? {
951
+ return Ok(());
952
+ }
953
+ }
954
+
955
+ Err(NaomeError::new(
956
+ "Cannot create a unique NAOME task worktree after 99 attempts.",
957
+ ))
958
+ }
959
+
960
+ fn task_worktree_name_head(root: &Path) -> Result<String, NaomeError> {
961
+ git_head(root)?.ok_or_else(|| {
962
+ NaomeError::new("Cannot create task worktree because the repository has no HEAD.")
963
+ })
964
+ }
965
+
903
966
  fn existing_naome_task_worktree_count(worktree_base: &Path) -> Result<usize, NaomeError> {
904
967
  let mut count = 0;
905
968
  for entry in fs::read_dir(worktree_base)? {
@@ -1469,10 +1469,10 @@ fn validate_commit_gate(
1469
1469
  .get("status")
1470
1470
  .and_then(Value::as_str)
1471
1471
  .unwrap_or("invalid");
1472
- if status == "complete" && completed_task_harness_refresh_diff(root)?.is_some() {
1473
- if is_safe_harness_refresh_diff(&changed_paths) {
1474
- return Ok(());
1475
- }
1472
+ if status == "complete" && is_deterministic_harness_refresh_diff(&changed_paths) {
1473
+ validate_pending_upgrade(task_state, root, errors)?;
1474
+ validate_completed_task_for_harness_refresh(task_state, root, &staged_entries, errors)?;
1475
+ return Ok(());
1476
1476
  }
1477
1477
 
1478
1478
  if status == "complete" {
@@ -1525,6 +1525,49 @@ fn validate_commit_gate(
1525
1525
  Ok(())
1526
1526
  }
1527
1527
 
1528
+ fn validate_completed_task_for_harness_refresh(
1529
+ task_state: &Value,
1530
+ root: &Path,
1531
+ staged_entries: &[ChangedEntry],
1532
+ errors: &mut Vec<String>,
1533
+ ) -> Result<(), NaomeError> {
1534
+ validate_active_task(task_state.get("activeTask"), errors);
1535
+ validate_active_task_references(task_state.get("activeTask"), root, errors, Some("complete"))?;
1536
+ if !task_state.get("blocker").is_some_and(Value::is_null) {
1537
+ errors.push("complete task state must have blocker set to null.".to_string());
1538
+ }
1539
+
1540
+ let Some(active_task) = task_state.get("activeTask") else {
1541
+ return Ok(());
1542
+ };
1543
+
1544
+ let check_ids = read_verification_check_ids(root, errors)?;
1545
+ validate_required_check_ids(active_task, &check_ids, errors);
1546
+
1547
+ let mut validation_errors = Vec::new();
1548
+ validate_complete_task_against_entries(
1549
+ active_task,
1550
+ root,
1551
+ &check_ids,
1552
+ staged_entries,
1553
+ &mut validation_errors,
1554
+ )?;
1555
+
1556
+ let staged_harness_paths = task_diff_from_entries(active_task, staged_entries).outside_paths;
1557
+ let allowed_scope_error = format!(
1558
+ "Changed files outside allowedPaths: {}",
1559
+ staged_harness_paths.join(", ")
1560
+ );
1561
+
1562
+ errors.extend(
1563
+ validation_errors
1564
+ .into_iter()
1565
+ .filter(|error| error != &allowed_scope_error),
1566
+ );
1567
+
1568
+ Ok(())
1569
+ }
1570
+
1528
1571
  fn validate_push_gate(task_state: &Value, errors: &mut Vec<String>) {
1529
1572
  let status = task_state
1530
1573
  .get("status")
@@ -1598,8 +1641,11 @@ fn is_safe_harness_refresh_path(path: &str) -> bool {
1598
1641
  is_packaged_machine_owned_path(path) || is_repair_support_path(path)
1599
1642
  }
1600
1643
 
1601
- fn is_safe_harness_refresh_diff(changed_paths: &[String]) -> bool {
1644
+ fn is_deterministic_harness_refresh_diff(changed_paths: &[String]) -> bool {
1602
1645
  !changed_paths.is_empty()
1646
+ && changed_paths
1647
+ .iter()
1648
+ .any(|path| is_packaged_machine_owned_path(path) || is_repair_archive_path(path))
1603
1649
  && changed_paths
1604
1650
  .iter()
1605
1651
  .all(|path| is_safe_harness_refresh_path(path))
@@ -662,6 +662,57 @@ fn execute_route_refuses_to_create_more_than_max_isolated_worktrees() {
662
662
  .contains("Too many NAOME task worktrees are present"));
663
663
  }
664
664
 
665
+ #[test]
666
+ fn execute_route_preflights_worktree_before_completed_task_baseline() {
667
+ let repo =
668
+ TestRepo::completed_task_with_unrelated_user_edit("route-completed-worktree-preflight");
669
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
670
+ let before_status = repo.git_status_short();
671
+ let common_dir = repo.git_stdout(&["rev-parse", "--git-common-dir"]);
672
+ let worktree_root = repo.path().join(common_dir).join("naome").join("worktrees");
673
+ fs::create_dir_all(&worktree_root).unwrap();
674
+ for index in 0..25 {
675
+ fs::create_dir_all(worktree_root.join(format!("stale-{index}"))).unwrap();
676
+ }
677
+
678
+ let error = evaluate_route(
679
+ repo.path(),
680
+ "Add another line to README as a new task.",
681
+ RouteOptions {
682
+ execute: true,
683
+ evaluation: EvaluationOptions::offline(),
684
+ },
685
+ )
686
+ .unwrap_err();
687
+
688
+ assert!(error
689
+ .to_string()
690
+ .contains("Too many NAOME task worktrees are present"));
691
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
692
+ assert_eq!(repo.git_status_short(), before_status);
693
+ }
694
+
695
+ #[test]
696
+ fn execute_route_uses_preflighted_worktree_name_after_completed_task_baseline() {
697
+ let repo = TestRepo::completed_task_with_unrelated_user_edit("route-worktree-name-preflight");
698
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
699
+ let before_short = &before_head[..12];
700
+
701
+ let route = evaluate_route(
702
+ repo.path(),
703
+ "Add another line to README as a new task.",
704
+ RouteOptions {
705
+ execute: true,
706
+ evaluation: EvaluationOptions::offline(),
707
+ },
708
+ )
709
+ .unwrap();
710
+
711
+ let worktree = route.worktree.expect("route should create a worktree");
712
+ assert!(worktree.branch.contains(before_short));
713
+ assert!(worktree.path.contains(before_short));
714
+ }
715
+
665
716
  #[test]
666
717
  fn dry_route_plans_harness_refresh_split_before_completed_task_baseline() {
667
718
  let repo = TestRepo::completed_task_with_harness_refresh_diff("route-dry-harness-refresh");
@@ -222,6 +222,146 @@ fn commit_gate_allows_staged_harness_refresh_split_from_completed_task() {
222
222
  assert!(report.errors.is_empty(), "{:#?}", report.errors);
223
223
  }
224
224
 
225
+ #[test]
226
+ fn commit_gate_allows_staged_harness_refresh_after_completed_task_is_baselined() {
227
+ let repo = TaskFixture::new(complete_task_state(json!({
228
+ "allowedPaths": ["README.md"],
229
+ "proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
230
+ })));
231
+ repo.install_healthy_harness();
232
+ repo.write("README.md", "# Completed task result\n");
233
+ repo.init_git();
234
+ repo.write("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
235
+ repo.write_json(
236
+ ".naome/manifest.json",
237
+ json!({
238
+ "name": "naome",
239
+ "harnessVersion": "1.1.1",
240
+ "profile": "standard",
241
+ "machineOwned": MACHINE_OWNED_PATHS,
242
+ "projectOwned": PROJECT_OWNED_PATHS,
243
+ "integrity": {}
244
+ }),
245
+ );
246
+ repo.git(["add", "AGENTS.md", ".naome/manifest.json"]);
247
+
248
+ let report = validate_task_state(
249
+ repo.path(),
250
+ TaskStateOptions {
251
+ mode: TaskStateMode::CommitGate,
252
+ ..TaskStateOptions::default()
253
+ },
254
+ )
255
+ .unwrap();
256
+
257
+ assert!(report.errors.is_empty(), "{:#?}", report.errors);
258
+ }
259
+
260
+ #[test]
261
+ fn commit_gate_rejects_harness_refresh_when_completed_task_proof_is_missing() {
262
+ let repo = TaskFixture::new(complete_task_state(json!({
263
+ "allowedPaths": ["README.md"],
264
+ "proofResults": []
265
+ })));
266
+ repo.install_healthy_harness();
267
+ repo.write("README.md", "# Completed task result\n");
268
+ repo.init_git();
269
+ repo.write("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
270
+ repo.git(["add", "AGENTS.md"]);
271
+
272
+ let report = validate_task_state(
273
+ repo.path(),
274
+ TaskStateOptions {
275
+ mode: TaskStateMode::CommitGate,
276
+ ..TaskStateOptions::default()
277
+ },
278
+ )
279
+ .unwrap();
280
+
281
+ assert!(
282
+ report
283
+ .errors
284
+ .iter()
285
+ .any(|error| error.contains("activeTask.proofResults missing proof result: diff-check")),
286
+ "{:#?}",
287
+ report.errors
288
+ );
289
+ }
290
+
291
+ #[test]
292
+ fn commit_gate_rejects_harness_refresh_with_pending_upgrade_state() {
293
+ let repo = TaskFixture::new(complete_task_state(json!({
294
+ "allowedPaths": ["README.md"],
295
+ "proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
296
+ })));
297
+ repo.install_healthy_harness();
298
+ repo.write("README.md", "# Completed task result\n");
299
+ repo.init_git();
300
+ repo.write("AGENTS.md", "# Agent Instructions\n\nUpdated by sync.\n");
301
+ repo.write_json(
302
+ ".naome/upgrade-state.json",
303
+ json!({
304
+ "status": "needs_agent_upgrade",
305
+ "fromVersion": "1.1.0",
306
+ "toVersion": "1.1.1",
307
+ "pending": ["manual-step"],
308
+ "completed": []
309
+ }),
310
+ );
311
+ repo.git(["add", "AGENTS.md", ".naome/upgrade-state.json"]);
312
+
313
+ let report = validate_task_state(
314
+ repo.path(),
315
+ TaskStateOptions {
316
+ mode: TaskStateMode::CommitGate,
317
+ ..TaskStateOptions::default()
318
+ },
319
+ )
320
+ .unwrap();
321
+
322
+ assert!(
323
+ report
324
+ .errors
325
+ .iter()
326
+ .any(|error| error.contains("NAOME upgrade is pending")),
327
+ "{:#?}",
328
+ report.errors
329
+ );
330
+ }
331
+
332
+ #[test]
333
+ fn commit_gate_rejects_support_only_harness_refresh_after_completed_task_is_baselined() {
334
+ let repo = TaskFixture::new(complete_task_state(json!({
335
+ "allowedPaths": ["README.md"],
336
+ "proofResults": [successful_proof(json!({ "evidence": ["README.md"] }))]
337
+ })));
338
+ repo.install_healthy_harness();
339
+ repo.write("README.md", "# Completed task result\n");
340
+ repo.init_git();
341
+ repo.write_json(
342
+ ".naome/upgrade-state.json",
343
+ json!({
344
+ "status": "complete",
345
+ "fromVersion": null,
346
+ "toVersion": "1.1.1",
347
+ "pending": [],
348
+ "completed": []
349
+ }),
350
+ );
351
+ repo.git(["add", ".naome/upgrade-state.json"]);
352
+
353
+ let report = validate_task_state(
354
+ repo.path(),
355
+ TaskStateOptions {
356
+ mode: TaskStateMode::CommitGate,
357
+ ..TaskStateOptions::default()
358
+ },
359
+ )
360
+ .unwrap();
361
+
362
+ assert!(!report.errors.is_empty(), "{:#?}", report.errors);
363
+ }
364
+
225
365
  #[test]
226
366
  fn commit_gate_ignores_unstaged_user_edits_outside_completed_task_scope() {
227
367
  let repo = TaskFixture::new(complete_task_state(json!({
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naome",
3
- "harnessVersion": "1.1.0",
3
+ "harnessVersion": "1.1.1",
4
4
  "profile": "standard",
5
5
  "installedAt": null,
6
6
  "machineOwned": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "status": "complete",
3
3
  "fromVersion": null,
4
- "toVersion": "1.1.0",
4
+ "toVersion": "1.1.1",
5
5
  "pending": [],
6
6
  "completed": []
7
7
  }