@lamentis/naome 1.4.5 → 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 (38) 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/common.rs +3 -3
  5. package/crates/naome-cli/tests/task_cli_local_state.rs +59 -0
  6. package/crates/naome-core/Cargo.toml +1 -1
  7. package/crates/naome-core/src/information_architecture.rs +1 -1
  8. package/crates/naome-core/src/install_plan.rs +2 -2
  9. package/crates/naome-core/src/route/execution_baselines.rs +3 -1
  10. package/crates/naome-core/src/route/git_ops.rs +48 -1
  11. package/crates/naome-core/src/route/worktree_files.rs +36 -1
  12. package/crates/naome-core/src/task_ledger.rs +13 -2
  13. package/crates/naome-core/src/task_state/commit_gate.rs +29 -0
  14. package/crates/naome-core/src/task_state/completed_refresh.rs +10 -2
  15. package/crates/naome-core/src/task_state/task_diff_api.rs +17 -3
  16. package/crates/naome-core/src/task_state/types.rs +1 -0
  17. package/crates/naome-core/tests/information_architecture.rs +1 -4
  18. package/crates/naome-core/tests/install_plan.rs +7 -0
  19. package/crates/naome-core/tests/repo_support/mod.rs +4 -3
  20. package/crates/naome-core/tests/route_baseline.rs +104 -0
  21. package/crates/naome-core/tests/route_worktree.rs +47 -1
  22. package/crates/naome-core/tests/task_ledger.rs +141 -203
  23. package/crates/naome-core/tests/task_ledger_support/mod.rs +206 -0
  24. package/crates/naome-core/tests/task_state.rs +38 -1
  25. package/crates/naome-core/tests/task_state_compact.rs +1 -1
  26. package/installer/harness-file-ops.js +6 -1
  27. package/installer/harness-files.js +10 -1
  28. package/native/darwin-arm64/naome +0 -0
  29. package/native/linux-x64/naome +0 -0
  30. package/package.json +1 -1
  31. package/templates/naome-root/.naome/bin/check-harness-health.js +4 -4
  32. package/templates/naome-root/.naome/bin/check-task-state.js +4 -4
  33. package/templates/naome-root/.naome/manifest.json +5 -6
  34. package/templates/naome-root/AGENTS.md +3 -1
  35. package/templates/naome-root/docs/naome/agent-workflow.md +4 -3
  36. package/templates/naome-root/docs/naome/architecture.md +2 -2
  37. package/templates/naome-root/docs/naome/execution.md +6 -6
  38. package/templates/naome-root/docs/naome/task-ledger.md +15 -11
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.4.5"
79
+ version = "1.4.6"
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.4.5"
87
+ version = "1.4.6"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.4.5"
3
+ version = "1.4.6"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -197,6 +197,7 @@ fn find_harness_root(start: &Path) -> Option<PathBuf> {
197
197
  &[
198
198
  &[".naome", "task-state.json"],
199
199
  &[".naome", "tasks", "active.json"],
200
+ &[".naome", "manifest.json"],
200
201
  ],
201
202
  )
202
203
  }
@@ -1,12 +1,12 @@
1
1
  use std::fs;
2
2
  use std::path::Path;
3
3
 
4
+ use naome_core::read_task_state_projection;
4
5
  use serde_json::{json, Value};
5
6
 
6
7
  pub(super) fn read_task_state(root: &Path) -> Result<Value, Box<dyn std::error::Error>> {
7
- Ok(serde_json::from_str(&fs::read_to_string(
8
- root.join(".naome/task-state.json"),
9
- )?)?)
8
+ read_task_state_projection(root)?
9
+ .ok_or_else(|| "NAOME task-state projection is unavailable.".into())
10
10
  }
11
11
 
12
12
  pub(super) fn write_task_state(
@@ -0,0 +1,59 @@
1
+ mod task_cli_support;
2
+
3
+ use std::fs;
4
+
5
+ use task_cli_support::{fixture_root, git, run_json, task_state, write_json};
6
+
7
+ #[test]
8
+ fn task_control_commands_work_without_task_state_projection_file() {
9
+ let root = fixture_root(task_state());
10
+ fs::remove_file(root.join(".naome/task-state.json")).unwrap();
11
+ write_json(
12
+ &root,
13
+ ".naome/manifest.json",
14
+ &serde_json::json!({
15
+ "schema": "naome.manifest.v1",
16
+ "name": "naome",
17
+ "harnessVersion": "1.4.6"
18
+ }),
19
+ );
20
+ git(&root, ["init"]);
21
+ git(&root, ["config", "user.email", "naome@example.com"]);
22
+ git(&root, ["config", "user.name", "NAOME Test"]);
23
+ git(
24
+ &root,
25
+ [
26
+ "add",
27
+ ".naomeignore",
28
+ ".naome/manifest.json",
29
+ ".naome/verification.json",
30
+ "README.md",
31
+ ],
32
+ );
33
+ git(&root, ["commit", "-m", "baseline"]);
34
+
35
+ let status = run_json(&root, ["task", "status", "--json"]);
36
+ assert_eq!(status["state"], "idle");
37
+ assert_eq!(status["taskId"], serde_json::Value::Null);
38
+ assert_eq!(status["proof"]["missingChecks"], serde_json::json!([]));
39
+
40
+ let proof_plan = run_json(&root, ["task", "proof-plan", "--json"]);
41
+ assert_eq!(proof_plan["state"], "idle");
42
+ assert_eq!(proof_plan["proof"]["requiredChecks"], serde_json::json!([]));
43
+
44
+ let snapshot = run_json(&root, ["task", "agent-snapshot", "--json"]);
45
+ assert_eq!(snapshot["task"]["state"], "idle");
46
+ assert_eq!(snapshot["nextAction"]["type"], "blocked");
47
+ assert_eq!(
48
+ snapshot["nextAction"]["primaryFindingId"],
49
+ "task.transition.no_active_task"
50
+ );
51
+
52
+ let commit_preflight = run_json(&root, ["task", "commit-preflight", "--json"]);
53
+ assert_eq!(commit_preflight["wouldPass"], false);
54
+ assert_eq!(commit_preflight["nextAction"]["type"], "blocked");
55
+ assert_eq!(
56
+ commit_preflight["nextAction"]["primaryFindingId"],
57
+ "task.transition.no_active_task"
58
+ );
59
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.4.5"
3
+ version = "1.4.6"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -39,7 +39,7 @@ const CLASSES: &[(&str, &str, &str, &str)] = &[
39
39
  (
40
40
  "generated_projection",
41
41
  "Compatibility views generated from durable state.",
42
- "commit_when_required_by_compatibility",
42
+ "never_commit",
43
43
  "regenerate_from_durable_state",
44
44
  ),
45
45
  (
@@ -21,7 +21,6 @@ pub const PROJECT_OWNED_PATHS: &[&str] = &[
21
21
  ".naomeignore",
22
22
  ".naome/init-state.json",
23
23
  ".naome/manifest.json",
24
- ".naome/task-state.json",
25
24
  ".naome/upgrade-state.json",
26
25
  ".naome/verification.json",
27
26
  ".naome/repository-model.json",
@@ -43,6 +42,7 @@ pub const LOCAL_ONLY_MACHINE_OWNED_PATHS: &[&str] = &[
43
42
  ".naome/bin/naome.js",
44
43
  ".naome/bin/check-task-state.js",
45
44
  ".naome/bin/check-harness-health.js",
45
+ ".naome/task-state.json",
46
46
  ".naome/task-contract.schema.json",
47
47
  "docs/naome/index.md",
48
48
  "docs/naome/first-run.md",
@@ -76,7 +76,7 @@ pub struct InstallPlan {
76
76
  }
77
77
 
78
78
  pub fn install_plan(harness_version: impl Into<String>) -> InstallPlan {
79
- let gitignore_entries = vec![".naome/task-journal.jsonl"];
79
+ let gitignore_entries = vec![".naome/task-journal.jsonl", ".naome/tasks/"];
80
80
  let mut git_exclude_entries = vec![
81
81
  "# NAOME local machine-owned harness files.",
82
82
  ".naome/archive/",
@@ -56,7 +56,9 @@ pub(super) fn baseline_harness_refresh_then_completed_task(
56
56
  "Unable to split harness refresh paths from completed task diff.",
57
57
  ));
58
58
  };
59
- commit_harness_paths(root, &split.harness_paths, execution)?;
59
+ let mut harness_paths = split.harness_paths;
60
+ harness_paths.extend(split.local_only_cleanup_paths);
61
+ commit_harness_paths(root, &harness_paths, execution)?;
60
62
  baseline_completed_task(
61
63
  root,
62
64
  "route_auto_baseline",
@@ -29,8 +29,10 @@ pub(super) fn git_add_completed_task_paths(root: &Path) -> Result<(), NaomeError
29
29
  }
30
30
 
31
31
  pub(super) fn git_stage_only_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
32
+ let cached_only_deletions = staged_deletions_with_local_files(root, paths)?;
32
33
  ensure_git_success(git_output(root, &["reset", "-q", "--", "."])?)?;
33
- git_add_paths(root, paths)
34
+ git_add_paths(root, paths)?;
35
+ git_rm_cached_paths(root, &cached_only_deletions)
34
36
  }
35
37
 
36
38
  fn git_add_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
@@ -81,6 +83,51 @@ fn git_ls_files(
81
83
  }
82
84
  }
83
85
 
86
+ fn staged_deletions_with_local_files(
87
+ root: &Path,
88
+ paths: &[String],
89
+ ) -> Result<Vec<String>, NaomeError> {
90
+ if paths.is_empty() {
91
+ return Ok(Vec::new());
92
+ }
93
+ let mut args = vec!["diff", "--name-status", "--cached", "-z", "--"];
94
+ args.extend(paths.iter().map(String::as_str));
95
+ let output = Command::new("git").args(args).current_dir(root).output()?;
96
+ if !output.status.success() {
97
+ return Err(NaomeError::new(command_output(&output)));
98
+ }
99
+
100
+ let mut deletions = Vec::new();
101
+ let tokens = split_nul_paths(&output.stdout);
102
+ let mut index = 0;
103
+ while index + 1 < tokens.len() {
104
+ let status = &tokens[index];
105
+ let path = tokens[index + 1].clone();
106
+ index += if status.starts_with('R') || status.starts_with('C') {
107
+ 3
108
+ } else {
109
+ 2
110
+ };
111
+ if status == "D" && root.join(&path).exists() && is_local_only_cleanup_path(&path) {
112
+ deletions.push(path);
113
+ }
114
+ }
115
+ Ok(deletions)
116
+ }
117
+
118
+ fn git_rm_cached_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
119
+ if paths.is_empty() {
120
+ return Ok(());
121
+ }
122
+ let mut args = vec!["rm", "--cached", "-q", "--ignore-unmatch", "--"];
123
+ args.extend(paths.iter().map(String::as_str));
124
+ ensure_git_success(Command::new("git").args(args).current_dir(root).output()?)
125
+ }
126
+
127
+ fn is_local_only_cleanup_path(path: &str) -> bool {
128
+ path == ".naome/task-state.json" || path.starts_with(".naome/tasks/")
129
+ }
130
+
84
131
  fn split_nul_paths(bytes: &[u8]) -> Vec<String> {
85
132
  String::from_utf8_lossy(bytes)
86
133
  .split('\0')
@@ -1,5 +1,6 @@
1
1
  use std::fs;
2
2
  use std::path::Path;
3
+ use std::process::Command;
3
4
 
4
5
  use crate::install_plan::{LOCAL_NATIVE_BINARY_PATHS, LOCAL_ONLY_MACHINE_OWNED_PATHS};
5
6
  use crate::models::NaomeError;
@@ -13,7 +14,10 @@ pub(super) fn copy_local_harness_files(
13
14
  paths.extend_from_slice(LOCAL_NATIVE_BINARY_PATHS);
14
15
 
15
16
  for relative_path in paths {
16
- if relative_path == ".naome/archive" || relative_path == ".naome/task-journal.jsonl" {
17
+ if relative_path == ".naome/archive"
18
+ || relative_path == ".naome/task-journal.jsonl"
19
+ || relative_path == ".naome/task-state.json"
20
+ {
17
21
  continue;
18
22
  }
19
23
 
@@ -27,6 +31,37 @@ pub(super) fn copy_local_harness_files(
27
31
  }
28
32
  fs::copy(&source, &destination)?;
29
33
  }
34
+ write_idle_projection_if_local_only(worktree_root)?;
30
35
 
31
36
  Ok(())
32
37
  }
38
+
39
+ fn write_idle_projection_if_local_only(worktree_root: &Path) -> Result<(), NaomeError> {
40
+ let output = Command::new("git")
41
+ .args([
42
+ "ls-files",
43
+ "--error-unmatch",
44
+ "--",
45
+ ".naome/task-state.json",
46
+ ])
47
+ .current_dir(worktree_root)
48
+ .output()?;
49
+ if output.status.success() {
50
+ return Ok(());
51
+ }
52
+
53
+ fs::write(
54
+ worktree_root.join(".naome/task-state.json"),
55
+ concat!(
56
+ "{\n",
57
+ " \"schema\": \"naome.task-state.v2\",\n",
58
+ " \"version\": 2,\n",
59
+ " \"status\": \"idle\",\n",
60
+ " \"activeTask\": null,\n",
61
+ " \"blocker\": null,\n",
62
+ " \"updatedAt\": \"1970-01-01T00:00:00.000Z\"\n",
63
+ "}\n"
64
+ ),
65
+ )?;
66
+ Ok(())
67
+ }
@@ -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");