@lamentis/naome 1.4.4 → 1.4.6

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 +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-cli/src/main.rs +1 -0
  4. package/crates/naome-cli/src/task_commands/agent_snapshot.rs +19 -70
  5. package/crates/naome-cli/src/task_commands/commit_preflight.rs +29 -41
  6. package/crates/naome-cli/src/task_commands/common.rs +3 -3
  7. package/crates/naome-cli/src/task_commands/loop_control.rs +40 -9
  8. package/crates/naome-cli/src/task_commands/planner/checks.rs +4 -2
  9. package/crates/naome-cli/src/task_commands/preflight.rs +8 -2
  10. package/crates/naome-cli/src/task_commands/record.rs +9 -0
  11. package/crates/naome-cli/src/task_commands/single_pass_action.rs +215 -0
  12. package/crates/naome-cli/src/task_commands/single_pass_action_fields.rs +101 -0
  13. package/crates/naome-cli/src/task_commands.rs +2 -0
  14. package/crates/naome-cli/tests/task_cli_fast_flow.rs +147 -2
  15. package/crates/naome-cli/tests/task_cli_local_state.rs +59 -0
  16. package/crates/naome-cli/tests/task_cli_review_fixes.rs +107 -0
  17. package/crates/naome-core/Cargo.toml +1 -1
  18. package/crates/naome-core/src/information_architecture.rs +1 -1
  19. package/crates/naome-core/src/install_plan.rs +2 -2
  20. package/crates/naome-core/src/route/execution_baselines.rs +3 -1
  21. package/crates/naome-core/src/route/git_ops.rs +48 -1
  22. package/crates/naome-core/src/route/worktree_files.rs +36 -1
  23. package/crates/naome-core/src/task_ledger.rs +13 -2
  24. package/crates/naome-core/src/task_state/commit_gate.rs +29 -0
  25. package/crates/naome-core/src/task_state/completed_refresh.rs +10 -2
  26. package/crates/naome-core/src/task_state/task_diff_api.rs +17 -3
  27. package/crates/naome-core/src/task_state/types.rs +1 -0
  28. package/crates/naome-core/tests/information_architecture.rs +1 -4
  29. package/crates/naome-core/tests/install_plan.rs +7 -0
  30. package/crates/naome-core/tests/repo_support/mod.rs +4 -3
  31. package/crates/naome-core/tests/route_baseline.rs +104 -0
  32. package/crates/naome-core/tests/route_worktree.rs +47 -1
  33. package/crates/naome-core/tests/task_ledger.rs +141 -203
  34. package/crates/naome-core/tests/task_ledger_support/mod.rs +206 -0
  35. package/crates/naome-core/tests/task_state.rs +38 -1
  36. package/crates/naome-core/tests/task_state_compact.rs +1 -1
  37. package/installer/harness-file-ops.js +6 -1
  38. package/installer/harness-files.js +10 -1
  39. package/native/darwin-arm64/naome +0 -0
  40. package/native/linux-x64/naome +0 -0
  41. package/package.json +1 -1
  42. package/templates/naome-root/.naome/bin/check-harness-health.js +4 -4
  43. package/templates/naome-root/.naome/bin/check-task-state.js +4 -4
  44. package/templates/naome-root/.naome/manifest.json +5 -6
  45. package/templates/naome-root/AGENTS.md +3 -1
  46. package/templates/naome-root/docs/naome/agent-workflow.md +4 -3
  47. package/templates/naome-root/docs/naome/architecture.md +2 -2
  48. package/templates/naome-root/docs/naome/execution.md +6 -6
  49. package/templates/naome-root/docs/naome/task-ledger.md +15 -11
@@ -7,7 +7,7 @@ mod write;
7
7
 
8
8
  use std::path::Path;
9
9
 
10
- use serde_json::Value;
10
+ use serde_json::{json, Value};
11
11
 
12
12
  use crate::models::NaomeError;
13
13
  use crate::paths;
@@ -36,7 +36,7 @@ pub fn read_task_state_projection(root: &Path) -> Result<Option<Value>, NaomeErr
36
36
  if let Some(projection) = rendered {
37
37
  return Ok(Some(projection.state));
38
38
  }
39
- Ok(legacy)
39
+ Ok(legacy.or_else(|| Some(idle_projection())))
40
40
  }
41
41
 
42
42
  pub fn render_task_state_from_ledger(
@@ -94,3 +94,14 @@ fn has_compact_proof_evidence(active_task: &Value) -> bool {
94
94
  .is_some_and(|entries| !entries.is_empty())
95
95
  })
96
96
  }
97
+
98
+ fn idle_projection() -> Value {
99
+ json!({
100
+ "schema": "naome.task-state.v2",
101
+ "version": 2,
102
+ "status": "idle",
103
+ "activeTask": null,
104
+ "blocker": null,
105
+ "updatedAt": "1970-01-01T00:00:00.000Z"
106
+ })
107
+ }
@@ -31,6 +31,10 @@ pub(super) fn validate_commit_gate(
31
31
  if changed_paths.is_empty() {
32
32
  return Ok(());
33
33
  }
34
+ validate_no_forced_local_runtime_commits(&staged_entries, errors);
35
+ if !errors.is_empty() {
36
+ return Ok(());
37
+ }
34
38
 
35
39
  let status = task_state
36
40
  .get("status")
@@ -93,6 +97,31 @@ pub(super) fn validate_commit_gate(
93
97
  Ok(())
94
98
  }
95
99
 
100
+ fn validate_no_forced_local_runtime_commits(
101
+ staged_entries: &[ChangedEntry],
102
+ errors: &mut Vec<String>,
103
+ ) {
104
+ for entry in staged_entries {
105
+ if is_allowed_local_runtime_cleanup(entry) {
106
+ continue;
107
+ }
108
+ if is_local_only_runtime_path(&entry.path) {
109
+ errors.push(format!(
110
+ "NAOME commit gate blocked local-only runtime path: {}. Keep it local-only; only staged rm --cached cleanup deletions are allowed.",
111
+ entry.path
112
+ ));
113
+ }
114
+ }
115
+ }
116
+
117
+ fn is_allowed_local_runtime_cleanup(entry: &ChangedEntry) -> bool {
118
+ entry.status == "deleted" && is_local_only_runtime_path(&entry.path)
119
+ }
120
+
121
+ fn is_local_only_runtime_path(path: &str) -> bool {
122
+ path == ".naome/task-state.json" || path.starts_with(".naome/tasks/")
123
+ }
124
+
96
125
  pub(super) fn validate_completed_task_for_harness_refresh(
97
126
  task_state: &Value,
98
127
  root: &Path,
@@ -3,8 +3,9 @@ use std::path::Path;
3
3
  use crate::models::NaomeError;
4
4
 
5
5
  use super::api::validate_task_state;
6
- use super::git_io::read_git_changed_paths;
6
+ use super::git_io::read_git_changed_entries;
7
7
  use super::repair::is_safe_harness_refresh_path;
8
+ use super::task_diff_api::is_local_only_untrack_cleanup;
8
9
  use super::types::{
9
10
  is_control_state_path, read_complete_active_task, CompletedTaskHarnessRefreshDiff,
10
11
  TaskStateMode, TaskStateOptions,
@@ -19,10 +20,16 @@ pub fn completed_task_harness_refresh_diff(
19
20
 
20
21
  let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
21
22
  let mut harness_paths = Vec::new();
23
+ let mut local_only_cleanup_paths = Vec::new();
22
24
  let mut task_paths = Vec::new();
23
25
  let mut other_paths = Vec::new();
24
26
 
25
- for path in read_git_changed_paths(root)? {
27
+ for entry in read_git_changed_entries(root)? {
28
+ if is_local_only_untrack_cleanup(&entry) {
29
+ local_only_cleanup_paths.push(entry.path);
30
+ continue;
31
+ }
32
+ let path = entry.path;
26
33
  if is_control_state_path(&path) {
27
34
  continue;
28
35
  }
@@ -57,6 +64,7 @@ pub fn completed_task_harness_refresh_diff(
57
64
  {
58
65
  Ok(Some(CompletedTaskHarnessRefreshDiff {
59
66
  harness_paths,
67
+ local_only_cleanup_paths,
60
68
  task_paths,
61
69
  }))
62
70
  } else {
@@ -16,8 +16,8 @@ use super::shape::{
16
16
  validate_pending_upgrade, validate_required_check_ids, validate_task_state_shape,
17
17
  };
18
18
  use super::types::{
19
- is_control_state_path, read_complete_active_task, CompletedTaskCommitDiff, HarnessRefreshDiff,
20
- HarnessRefreshWithUnrelatedDiff, TaskStateOptions,
19
+ is_control_state_path, is_local_runtime_path, read_complete_active_task, ChangedEntry,
20
+ CompletedTaskCommitDiff, HarnessRefreshDiff, HarnessRefreshWithUnrelatedDiff, TaskStateOptions,
21
21
  };
22
22
  use super::util::{matches_any_pattern, string_array};
23
23
 
@@ -36,9 +36,17 @@ pub fn completed_task_commit_diff(
36
36
 
37
37
  let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
38
38
  let mut task_entries = Vec::new();
39
+ let mut local_only_cleanup_paths = Vec::new();
39
40
  let mut unrelated_paths = Vec::new();
40
41
  for entry in read_git_changed_entries(root)? {
41
- if is_control_state_path(&entry.path) || matches_any_pattern(&entry.path, &allowed_paths) {
42
+ if is_local_only_untrack_cleanup(&entry) {
43
+ local_only_cleanup_paths.push(entry.path);
44
+ continue;
45
+ }
46
+ if is_control_state_path(&entry.path) || is_local_runtime_path(&entry.path) {
47
+ continue;
48
+ }
49
+ if matches_any_pattern(&entry.path, &allowed_paths) {
42
50
  task_entries.push(entry);
43
51
  } else {
44
52
  unrelated_paths.push(entry.path);
@@ -73,6 +81,7 @@ pub fn completed_task_commit_diff(
73
81
  let mut task_paths: Vec<String> = task_entries
74
82
  .into_iter()
75
83
  .map(|entry| entry.path)
84
+ .chain(local_only_cleanup_paths)
76
85
  .collect::<HashSet<_>>()
77
86
  .into_iter()
78
87
  .collect();
@@ -85,6 +94,11 @@ pub fn completed_task_commit_diff(
85
94
  }))
86
95
  }
87
96
 
97
+ pub(super) fn is_local_only_untrack_cleanup(entry: &ChangedEntry) -> bool {
98
+ (entry.path == ".naome/task-state.json" || entry.path.starts_with(".naome/tasks/"))
99
+ && entry.status == "deleted"
100
+ }
101
+
88
102
  pub fn harness_refresh_diff(root: &Path) -> Result<Option<HarnessRefreshDiff>, NaomeError> {
89
103
  let changed_paths = read_git_changed_paths(root)?;
90
104
  let has_repair_signal = changed_paths
@@ -67,6 +67,7 @@ pub(super) struct ChangedEntry {
67
67
  #[derive(Debug, Clone, PartialEq, Eq)]
68
68
  pub struct CompletedTaskHarnessRefreshDiff {
69
69
  pub harness_paths: Vec<String>,
70
+ pub local_only_cleanup_paths: Vec<String>,
70
71
  pub task_paths: Vec<String>,
71
72
  }
72
73
 
@@ -9,10 +9,7 @@ fn classifies_naome_information_by_restore_source() {
9
9
 
10
10
  let projection = classify_information_path(".naome/task-state.json");
11
11
  assert_eq!(projection.class, "generated_projection");
12
- assert_eq!(
13
- projection.commit_policy,
14
- "commit_when_required_by_compatibility"
15
- );
12
+ assert_eq!(projection.commit_policy, "never_commit");
16
13
  assert_eq!(projection.restore_policy, "regenerate_from_durable_state");
17
14
 
18
15
  let local = classify_information_path(".naome/tmp/route.prompt");
@@ -9,6 +9,10 @@ fn install_plan_marks_machine_docs_and_bins_local_only() {
9
9
  assert!(plan.machine_owned.contains(&"docs/naome/execution.md"));
10
10
  assert!(plan.project_owned.contains(&"docs/naome/architecture.md"));
11
11
  assert!(!plan.project_owned.contains(&".naome/tasks/**"));
12
+ assert!(!plan.project_owned.contains(&".naome/task-state.json"));
13
+ assert!(plan
14
+ .local_only_machine_owned
15
+ .contains(&".naome/task-state.json"));
12
16
  assert!(plan
13
17
  .local_only_machine_owned
14
18
  .contains(&".naome/bin/check-task-state.js"));
@@ -28,6 +32,7 @@ fn install_plan_includes_git_exclude_and_untrack_policy() {
28
32
  assert!(plan
29
33
  .gitignore_entries
30
34
  .contains(&".naome/task-journal.jsonl"));
35
+ assert!(plan.gitignore_entries.contains(&".naome/tasks/"));
31
36
  assert!(plan.git_exclude_entries.contains(&".naome/archive/"));
32
37
  assert!(plan.git_exclude_entries.contains(&".naome/cache/"));
33
38
  assert!(plan.git_exclude_entries.contains(&".naome/tasks/"));
@@ -35,6 +40,7 @@ fn install_plan_includes_git_exclude_and_untrack_policy() {
35
40
  assert!(plan
36
41
  .git_exclude_entries
37
42
  .contains(&".naome/task-journal.jsonl"));
43
+ assert!(plan.git_exclude_entries.contains(&".naome/task-state.json"));
38
44
  assert!(plan
39
45
  .git_exclude_entries
40
46
  .contains(&".naome/bin/check-harness-health.js"));
@@ -45,5 +51,6 @@ fn install_plan_includes_git_exclude_and_untrack_policy() {
45
51
  assert!(plan
46
52
  .git_untrack_paths
47
53
  .contains(&".naome/task-journal.jsonl"));
54
+ assert!(plan.git_untrack_paths.contains(&".naome/task-state.json"));
48
55
  assert!(plan.git_untrack_paths.contains(&"docs/naome/first-run.md"));
49
56
  }
@@ -14,8 +14,9 @@ pub use routes::{
14
14
  try_route_readme_task,
15
15
  };
16
16
  pub use verification_values::{
17
- change_type, check_missing_last_verified_fixture, diff_check, minimal_task_state,
18
- placeholder_verification_contract_fixture, quality_check, repo_docs_verification_fixture,
19
- repository_quality_config_source, repository_quality_config_value, repository_semantic_check,
17
+ change_type, check_missing_last_verified_fixture, completed_task_state, diff_check,
18
+ minimal_task_state, placeholder_verification_contract_fixture, quality_check,
19
+ repo_docs_verification_fixture, repository_quality_config_source,
20
+ repository_quality_config_value, repository_semantic_check,
20
21
  semantic_repository_quality_fixture_source, verification_value,
21
22
  };
@@ -78,6 +78,110 @@ fn execute_route_auto_baselines_then_admits_next_task_and_writes_local_journal()
78
78
  assert!(journal.contains("\"outcome\":\"route_auto_baseline\""));
79
79
  }
80
80
 
81
+ #[test]
82
+ fn execute_route_preserves_cached_projection_untrack_cleanup() {
83
+ let repo = TestRepo::completed_task_with_diff("route-projection-untrack-cleanup");
84
+ let git_dir = repo.git_stdout(&["rev-parse", "--git-dir"]);
85
+ std::fs::write(
86
+ repo.path().join(git_dir).join("info").join("exclude"),
87
+ ".naome/task-state.json\n",
88
+ )
89
+ .unwrap();
90
+ repo.git(&["rm", "--cached", "-q", "--", ".naome/task-state.json"]);
91
+
92
+ let route = route_new_task(&repo, "Start a new task for README polish.", true);
93
+
94
+ assert_eq!(
95
+ route.policy_action,
96
+ "auto_commit_completed_task_then_create_new_task"
97
+ );
98
+ assert!(route.mutation_performed);
99
+ assert!(repo.git_status_short().is_empty());
100
+ let committed_paths = repo.git_stdout(&["show", "--name-status", "--format=", "HEAD"]);
101
+ assert!(committed_paths.contains("M\tREADME.md"));
102
+ assert!(committed_paths.contains("D\t.naome/task-state.json"));
103
+ }
104
+
105
+ #[test]
106
+ fn execute_route_preserves_cached_ledger_untrack_cleanup() {
107
+ let repo = TestRepo::completed_task_with_diff("route-ledger-untrack-cleanup");
108
+ let git_dir = repo.git_stdout(&["rev-parse", "--git-dir"]);
109
+ std::fs::write(
110
+ repo.path().join(git_dir).join("info").join("exclude"),
111
+ ".naome/tasks/\n",
112
+ )
113
+ .unwrap();
114
+ repo.write_file(".naome/tasks/active.json", "{}\n");
115
+ repo.git(&["add", "-f", ".naome/tasks/active.json"]);
116
+ repo.git(&["commit", "-m", "track ledger runtime"]);
117
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
118
+ repo.write_base_naome_state(repo_support::completed_task_state(&admission_head));
119
+ repo.git(&["add", ".naome/task-state.json"]);
120
+ repo.git(&["commit", "-m", "refresh task state"]);
121
+ repo.git(&["rm", "--cached", "-q", "--", ".naome/tasks/active.json"]);
122
+
123
+ let route = route_new_task(&repo, "Start a new task for README polish.", true);
124
+
125
+ assert_eq!(
126
+ route.policy_action,
127
+ "auto_commit_completed_task_then_create_new_task"
128
+ );
129
+ assert!(route.mutation_performed);
130
+ assert!(repo.git_status_short().is_empty());
131
+ let committed_paths = repo.git_stdout(&["show", "--name-status", "--format=", "HEAD"]);
132
+ assert!(committed_paths.contains("M\tREADME.md"));
133
+ assert!(committed_paths.contains("D\t.naome/tasks/active.json"));
134
+ }
135
+
136
+ #[test]
137
+ fn execute_route_preserves_projection_cleanup_in_harness_split_baseline() {
138
+ let repo = TestRepo::completed_task_with_harness_refresh_diff(
139
+ "route-harness-split-projection-cleanup",
140
+ );
141
+ let git_dir = repo.git_stdout(&["rev-parse", "--git-dir"]);
142
+ std::fs::write(
143
+ repo.path().join(git_dir).join("info").join("exclude"),
144
+ ".naome/task-state.json\n",
145
+ )
146
+ .unwrap();
147
+ repo.git(&["rm", "--cached", "-q", "--", ".naome/task-state.json"]);
148
+
149
+ let route = route_new_task(&repo, "Start a new task for README polish.", true);
150
+
151
+ assert_eq!(
152
+ route.policy_action,
153
+ "auto_commit_harness_refresh_then_completed_task_then_create_new_task"
154
+ );
155
+ assert_eq!(
156
+ route.executed_actions,
157
+ vec![
158
+ "commit_harness_refresh_baseline".to_string(),
159
+ "commit_task_baseline".to_string(),
160
+ ]
161
+ );
162
+ assert!(repo.git_status_short().is_empty());
163
+ let harness_commit_paths = repo.git_stdout(&["show", "--name-status", "--format=", "HEAD^"]);
164
+ assert!(harness_commit_paths.contains("D\t.naome/task-state.json"));
165
+ }
166
+
167
+ #[test]
168
+ fn execute_route_does_not_preserve_cached_product_file_deletion_cleanup() {
169
+ let repo = TestRepo::completed_task_with_diff("route-product-untrack-ignored");
170
+ repo.git(&["rm", "--cached", "-q", "--", "README.md"]);
171
+
172
+ let route = route_new_task(&repo, "Start a new task for README polish.", true);
173
+
174
+ assert_eq!(
175
+ route.policy_action,
176
+ "auto_commit_completed_task_then_create_new_task"
177
+ );
178
+ assert!(route.mutation_performed);
179
+ assert!(repo.git_status_short().is_empty());
180
+ let committed_paths = repo.git_stdout(&["show", "--name-status", "--format=", "HEAD"]);
181
+ assert!(committed_paths.contains("M\tREADME.md"));
182
+ assert!(!committed_paths.contains("D\tREADME.md"));
183
+ }
184
+
81
185
  #[test]
82
186
  fn execute_route_baselines_completed_task_and_creates_worktree_for_unrelated_user_edit() {
83
187
  let repo = TestRepo::completed_task_with_unrelated_user_edit("route-preserve-user-edit");
@@ -1,8 +1,13 @@
1
1
  use serde_json::json;
2
+ use std::fs;
2
3
 
3
4
  mod repo_support;
4
5
 
5
- use repo_support::{route_readme_task, try_route_readme_task, TestRepo};
6
+ use naome_core::read_task_state_projection;
7
+ use repo_support::{
8
+ completed_task_state, diff_check, route_readme_task, try_route_readme_task, verification_value,
9
+ TestRepo,
10
+ };
6
11
 
7
12
  #[test]
8
13
  fn execute_route_refuses_to_create_more_than_max_isolated_worktrees() {
@@ -52,3 +57,44 @@ fn execute_route_uses_preflighted_worktree_name_after_completed_task_baseline()
52
57
  assert!(worktree.branch.contains(before_short));
53
58
  assert!(worktree.path.contains(before_short));
54
59
  }
60
+
61
+ #[test]
62
+ fn execute_route_resets_local_task_state_projection_in_isolated_worktree() {
63
+ let repo = TestRepo::new("route-worktree-no-stale-projection");
64
+ repo.init_git();
65
+ repo.write_file(".naomeignore", ".naome/archive/\n.naome/tasks/\n");
66
+ repo.write_file("README.md", "# Baseline\n");
67
+ repo.write_file("USER.md", "user baseline\n");
68
+ repo.write_naome_json(
69
+ "init-state.json",
70
+ json!({ "initialized": true, "intakeStatus": "complete" }),
71
+ );
72
+ repo.write_naome_json("upgrade-state.json", json!({ "status": "complete" }));
73
+ repo.write_naome_json(
74
+ "verification.json",
75
+ verification_value("ready", vec![diff_check(vec!["README.md"])], vec![]),
76
+ );
77
+ repo.git(&["add", "."]);
78
+ repo.git(&["commit", "-m", "baseline"]);
79
+ let git_dir = repo.git_stdout(&["rev-parse", "--git-dir"]);
80
+ std::fs::write(
81
+ repo.path().join(git_dir).join("info").join("exclude"),
82
+ ".naome/task-state.json\n",
83
+ )
84
+ .unwrap();
85
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
86
+ repo.write_naome_json("task-state.json", completed_task_state(&admission_head));
87
+ repo.write_file("README.md", "# Changed\n");
88
+ repo.write_file("USER.md", "user local edit\n");
89
+
90
+ let route = route_readme_task(&repo, true);
91
+ let task_root = route.task_root.as_str();
92
+
93
+ assert_ne!(task_root, repo.path().to_string_lossy().as_ref());
94
+ assert!(fs::exists(std::path::Path::new(task_root).join(".naome/task-state.json")).unwrap());
95
+ let projection = read_task_state_projection(std::path::Path::new(task_root))
96
+ .unwrap()
97
+ .unwrap();
98
+ assert_eq!(projection["status"], "idle");
99
+ assert_eq!(projection["activeTask"], serde_json::Value::Null);
100
+ }