@lamentis/naome 1.2.0 → 1.2.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.
Files changed (113) hide show
  1. package/Cargo.lock +2 -2
  2. package/bin/naome-node.js +2 -1579
  3. package/bin/naome.js +19 -5
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/dispatcher.rs +2 -1
  6. package/crates/naome-cli/src/main.rs +3 -0
  7. package/crates/naome-cli/src/quality_commands.rs +90 -2
  8. package/crates/naome-core/Cargo.toml +1 -1
  9. package/crates/naome-core/src/decision/checks.rs +64 -0
  10. package/crates/naome-core/src/decision/idle.rs +67 -0
  11. package/crates/naome-core/src/decision/json.rs +36 -0
  12. package/crates/naome-core/src/decision/states.rs +165 -0
  13. package/crates/naome-core/src/decision.rs +131 -353
  14. package/crates/naome-core/src/install_plan.rs +2 -0
  15. package/crates/naome-core/src/lib.rs +5 -3
  16. package/crates/naome-core/src/paths.rs +3 -1
  17. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  18. package/crates/naome-core/src/quality/adapters.rs +20 -67
  19. package/crates/naome-core/src/quality/cleanup.rs +13 -1
  20. package/crates/naome-core/src/quality/config.rs +8 -15
  21. package/crates/naome-core/src/quality/config_support.rs +24 -0
  22. package/crates/naome-core/src/quality/mod.rs +18 -0
  23. package/crates/naome-core/src/quality/scanner.rs +20 -8
  24. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  25. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  26. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  27. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  28. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  29. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  30. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  31. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  32. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  33. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  34. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  35. package/crates/naome-core/src/quality/types.rs +3 -0
  36. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  37. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  38. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  39. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  40. package/crates/naome-core/src/route/context.rs +180 -0
  41. package/crates/naome-core/src/route/execution.rs +96 -0
  42. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  43. package/crates/naome-core/src/route/execution_support.rs +57 -0
  44. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  45. package/crates/naome-core/src/route/git_ops.rs +72 -0
  46. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  47. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  48. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  49. package/crates/naome-core/src/route/worktree.rs +75 -0
  50. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  51. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  52. package/crates/naome-core/src/route.rs +44 -1217
  53. package/crates/naome-core/src/verification.rs +1 -0
  54. package/crates/naome-core/tests/decision.rs +24 -118
  55. package/crates/naome-core/tests/harness_health.rs +2 -0
  56. package/crates/naome-core/tests/quality.rs +12 -118
  57. package/crates/naome-core/tests/quality_structure.rs +116 -0
  58. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  59. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  60. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  61. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  62. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  63. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  64. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  65. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  66. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  67. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  68. package/crates/naome-core/tests/route.rs +1 -1376
  69. package/crates/naome-core/tests/route_baseline.rs +86 -0
  70. package/crates/naome-core/tests/route_completion.rs +141 -0
  71. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  72. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  73. package/crates/naome-core/tests/route_worktree.rs +54 -0
  74. package/crates/naome-core/tests/task_state.rs +60 -432
  75. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  76. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  77. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  78. package/crates/naome-core/tests/verification.rs +4 -45
  79. package/crates/naome-core/tests/verification_contract.rs +22 -78
  80. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  81. package/installer/agents.js +90 -0
  82. package/installer/context.js +67 -0
  83. package/installer/filesystem.js +166 -0
  84. package/installer/flows.js +84 -0
  85. package/installer/git-boundary.js +170 -0
  86. package/installer/git-hook-content.js +36 -0
  87. package/installer/git-hooks.js +134 -0
  88. package/installer/git-local.js +2 -0
  89. package/installer/git-shared.js +35 -0
  90. package/installer/harness-file-ops.js +140 -0
  91. package/installer/harness-files.js +56 -0
  92. package/installer/harness-verification.js +123 -0
  93. package/installer/install-plan.js +66 -0
  94. package/installer/main.js +25 -0
  95. package/installer/manifest-state.js +167 -0
  96. package/installer/native-build.js +24 -0
  97. package/installer/native-format.js +6 -0
  98. package/installer/native.js +162 -0
  99. package/installer/output.js +131 -0
  100. package/installer/version.js +32 -0
  101. package/native/darwin-arm64/naome +0 -0
  102. package/native/linux-x64/naome +0 -0
  103. package/package.json +2 -1
  104. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  105. package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
  106. package/templates/naome-root/.naome/bin/naome.js +25 -21
  107. package/templates/naome-root/.naome/manifest.json +4 -2
  108. package/templates/naome-root/.naome/repository-structure.json +90 -0
  109. package/templates/naome-root/.naome/verification.json +1 -0
  110. package/templates/naome-root/docs/naome/index.md +4 -3
  111. package/templates/naome-root/docs/naome/repository-quality.md +3 -0
  112. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  113. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -0,0 +1,86 @@
1
+ use serde_json::json;
2
+ use std::fs;
3
+
4
+ mod repo_support;
5
+
6
+ use repo_support::{
7
+ assert_commit_paths, assert_isolated_worktree_ready, route_new_task, route_readme_task,
8
+ TestRepo,
9
+ };
10
+
11
+ #[test]
12
+ fn dry_route_reports_auto_baseline_without_mutating() {
13
+ let repo = TestRepo::completed_task_with_diff("route-dry-auto");
14
+ let before = repo.git_stdout(&["rev-parse", "HEAD"]);
15
+
16
+ let route = route_new_task(&repo, "Start a new task for README polish.", false);
17
+
18
+ assert_eq!(
19
+ route.policy_action,
20
+ "auto_commit_completed_task_then_create_new_task"
21
+ );
22
+ assert!(!route.mutation_performed);
23
+ assert!(!route.can_create_task);
24
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
25
+ assert!(!route.user_message.contains("commit_task_baseline"));
26
+ assert!(route.human_options.is_empty());
27
+ }
28
+
29
+ #[test]
30
+ fn execute_route_auto_baselines_then_admits_next_task_and_writes_local_journal() {
31
+ let repo = TestRepo::completed_task_with_diff("route-execute-auto");
32
+
33
+ let route = route_new_task(&repo, "Start a new task for README polish.", true);
34
+
35
+ assert_eq!(
36
+ route.policy_action,
37
+ "auto_commit_completed_task_then_create_new_task"
38
+ );
39
+ assert!(route.mutation_performed);
40
+ assert!(route.can_create_task);
41
+ assert_eq!(route.next_decision.state, "ready_for_task");
42
+ assert!(route
43
+ .executed_actions
44
+ .contains(&"commit_task_baseline".to_string()));
45
+ assert!(repo.git_status_short().is_empty());
46
+
47
+ let journal = fs::read_to_string(repo.path().join(".naome/task-journal.jsonl")).unwrap();
48
+ assert!(journal.contains("\"taskId\":\"readme-task\""));
49
+ assert!(journal.contains("\"outcome\":\"route_auto_baseline\""));
50
+ }
51
+
52
+ #[test]
53
+ fn execute_route_baselines_completed_task_and_creates_worktree_for_unrelated_user_edit() {
54
+ let repo = TestRepo::completed_task_with_unrelated_user_edit("route-preserve-user-edit");
55
+
56
+ let route = route_readme_task(&repo, true);
57
+
58
+ assert_eq!(
59
+ route.policy_action,
60
+ "auto_commit_completed_task_then_create_isolated_task_worktree"
61
+ );
62
+ assert!(route.mutation_performed);
63
+ assert!(route.user_message.contains("isolated task worktree"));
64
+ assert!(route.human_options.is_empty());
65
+ assert_ne!(route.task_root, repo.path().to_string_lossy());
66
+ assert_isolated_worktree_ready(&route, &repo, "M USER.md");
67
+ assert_commit_paths(&repo, &["README.md"], &["USER.md"]);
68
+ }
69
+
70
+ #[test]
71
+ fn execute_route_creates_worktree_for_dirty_repo_new_task() {
72
+ let repo = TestRepo::new("route-dirty-worktree");
73
+ repo.init_git();
74
+ repo.write_file("README.md", "# Baseline\n");
75
+ repo.write_file("USER.md", "user baseline\n");
76
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
77
+ repo.git(&["add", "."]);
78
+ repo.git(&["commit", "-m", "baseline"]);
79
+ repo.write_file("USER.md", "user local edit\n");
80
+
81
+ let route = route_readme_task(&repo, true);
82
+
83
+ assert_eq!(route.policy_action, "create_isolated_task_worktree");
84
+ assert!(route.mutation_performed);
85
+ assert_isolated_worktree_ready(&route, &repo, "M USER.md");
86
+ }
@@ -0,0 +1,141 @@
1
+ use std::fs;
2
+
3
+ use naome_core::{evaluate_route, explain_route, EvaluationOptions, RouteOptions};
4
+ use serde_json::json;
5
+
6
+ mod repo_support;
7
+
8
+ use repo_support::TestRepo;
9
+
10
+ #[test]
11
+ fn execute_route_does_not_mutate_when_prompt_blocks_commit() {
12
+ let repo = TestRepo::completed_task_with_diff("route-no-commit");
13
+ let before = repo.git_stdout(&["rev-parse", "HEAD"]);
14
+
15
+ let route = evaluate_route(
16
+ repo.path(),
17
+ "Do not commit. Start a new task after this.",
18
+ RouteOptions {
19
+ execute: true,
20
+ evaluation: EvaluationOptions::offline(),
21
+ },
22
+ )
23
+ .unwrap();
24
+
25
+ assert_eq!(route.policy_action, "block_auto_baseline_due_to_no_commit");
26
+ assert!(!route.mutation_performed);
27
+ assert!(!route.can_create_task);
28
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
29
+ assert!(repo.git_status_short().contains("README.md"));
30
+ }
31
+
32
+ #[test]
33
+ fn explicit_route_commit_baseline_leaves_unrelated_user_edit_unstaged() {
34
+ let repo = TestRepo::completed_task_with_diff("route-commit-task-scope");
35
+ repo.write_file("USER.md", "user baseline\n");
36
+ repo.git(&["add", "USER.md"]);
37
+ repo.git(&["commit", "-m", "user baseline"]);
38
+ repo.write_file("README.md", "# Changed\n");
39
+ repo.write_file("USER.md", "user local edit\n");
40
+ repo.git(&["add", "USER.md"]);
41
+
42
+ let route = evaluate_route(
43
+ repo.path(),
44
+ "commit_task_baseline",
45
+ RouteOptions {
46
+ execute: true,
47
+ evaluation: EvaluationOptions::offline(),
48
+ },
49
+ )
50
+ .unwrap();
51
+
52
+ assert_eq!(route.policy_action, "commit_task_baseline");
53
+ assert!(route.mutation_performed);
54
+ assert_eq!(repo.git_status_short(), "M USER.md");
55
+
56
+ let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
57
+ assert!(committed_paths.contains("README.md"));
58
+ assert!(!committed_paths.contains("USER.md"));
59
+ }
60
+
61
+ #[test]
62
+ fn execute_route_journals_external_commit_after_completed_task() {
63
+ let repo = TestRepo::completed_task_with_diff("route-external-commit");
64
+ repo.git(&["add", "-A"]);
65
+ repo.git(&["commit", "-m", "manual baseline"]);
66
+ assert!(repo.git_status_short().is_empty());
67
+
68
+ let route = evaluate_route(
69
+ repo.path(),
70
+ "Create a new task for README polish.",
71
+ RouteOptions {
72
+ execute: true,
73
+ evaluation: EvaluationOptions::offline(),
74
+ },
75
+ )
76
+ .unwrap();
77
+
78
+ assert_eq!(route.repo_state_before, "ready_for_task");
79
+ assert!(route.mutation_performed);
80
+ assert!(route.can_create_task);
81
+ assert!(route
82
+ .executed_actions
83
+ .contains(&"journal_external_task_baseline".to_string()));
84
+
85
+ let journal = fs::read_to_string(repo.path().join(".naome/task-journal.jsonl")).unwrap();
86
+ assert!(journal.contains("\"outcome\":\"external_baseline\""));
87
+ assert!(journal.contains("\"taskId\":\"readme-task\""));
88
+ }
89
+
90
+ #[test]
91
+ fn explain_reports_winning_rule_and_mutation_plan_without_executing() {
92
+ let repo = TestRepo::completed_task_with_diff("route-explain");
93
+ let before = repo.git_stdout(&["rev-parse", "HEAD"]);
94
+
95
+ let explain = explain_route(
96
+ repo.path(),
97
+ "Start a new task for README polish.",
98
+ EvaluationOptions::offline(),
99
+ )
100
+ .unwrap();
101
+
102
+ assert_eq!(
103
+ explain.winning_rule,
104
+ "completed_task_valid_new_task_auto_baseline"
105
+ );
106
+ assert!(explain.would_mutate);
107
+ assert!(explain
108
+ .discarded_candidate_actions
109
+ .contains(&"review_task_diff".to_string()));
110
+ assert!(explain
111
+ .required_context
112
+ .contains(&"docs/naome/execution.md".to_string()));
113
+ assert!(!explain.user_message.contains("commit_task_baseline"));
114
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
115
+ }
116
+
117
+ #[test]
118
+ fn unhealthy_harness_route_blocks_normal_work() {
119
+ let repo = TestRepo::new("route-unhealthy");
120
+ repo.init_git();
121
+ repo.write_file("README.md", "# Baseline\n");
122
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
123
+ repo.write_file(".naome/bin/check-harness-health.js", "process.exit(1);\n");
124
+ repo.git(&["add", "."]);
125
+ repo.git(&["commit", "-m", "baseline"]);
126
+
127
+ let route = evaluate_route(
128
+ repo.path(),
129
+ "Create a new task.",
130
+ RouteOptions {
131
+ execute: true,
132
+ evaluation: EvaluationOptions::online(),
133
+ },
134
+ )
135
+ .unwrap();
136
+
137
+ assert_eq!(route.repo_state_before, "harness_unhealthy");
138
+ assert!(!route.mutation_performed);
139
+ assert!(!route.can_create_task);
140
+ assert!(!route.human_options.is_empty());
141
+ }
@@ -0,0 +1,135 @@
1
+ use std::fs;
2
+
3
+ use naome_core::{evaluate_route, EvaluationOptions, RouteOptions};
4
+
5
+ mod repo_support;
6
+
7
+ use repo_support::{
8
+ assert_commit_paths, assert_isolated_worktree_ready, route_new_task, route_readme_task,
9
+ TestRepo,
10
+ };
11
+
12
+ #[test]
13
+ fn execute_route_baselines_harness_refresh_before_dirty_repo_worktree() {
14
+ let repo = TestRepo::dirty_harness_refresh_repo("route-dirty-harness-refresh-worktree", true);
15
+
16
+ let route = route_readme_task(&repo, true);
17
+
18
+ assert_eq!(
19
+ route.policy_action,
20
+ "auto_commit_harness_refresh_then_create_isolated_task_worktree"
21
+ );
22
+ assert_eq!(
23
+ route.executed_actions,
24
+ vec![
25
+ "commit_harness_refresh_baseline".to_string(),
26
+ "create_task_worktree".to_string()
27
+ ]
28
+ );
29
+ assert_isolated_worktree_ready(&route, &repo, "M USER.md");
30
+ assert_commit_paths(&repo, &["AGENTS.md", ".naome/manifest.json"], &["USER.md"]);
31
+ }
32
+
33
+ #[test]
34
+ fn execute_route_baselines_pure_harness_refresh_before_new_task_without_worktree() {
35
+ let repo = TestRepo::completed_task_with_harness_refresh_only(
36
+ "route-pure-harness-refresh-no-worktree",
37
+ );
38
+
39
+ let route = route_readme_task(&repo, true);
40
+
41
+ assert_eq!(
42
+ route.policy_action,
43
+ "auto_commit_harness_refresh_then_create_new_task"
44
+ );
45
+ assert_eq!(
46
+ route.executed_actions,
47
+ vec!["commit_harness_refresh_baseline".to_string()]
48
+ );
49
+ assert!(route.mutation_performed);
50
+ assert!(route.can_create_task);
51
+ assert!(route.worktree.is_none());
52
+ assert_eq!(route.task_root, repo.path().to_string_lossy());
53
+ assert_eq!(route.next_decision.state, "ready_for_task");
54
+ assert_eq!(repo.git_status_short(), "");
55
+
56
+ assert_commit_paths(
57
+ &repo,
58
+ &["AGENTS.md", ".naome/manifest.json"],
59
+ &["README.md"],
60
+ );
61
+ }
62
+
63
+ #[test]
64
+ fn execute_route_repair_request_baselines_harness_refresh_only() {
65
+ let repo = TestRepo::dirty_harness_refresh_repo("route-harness-refresh-repair-only", true);
66
+
67
+ let route = evaluate_route(
68
+ repo.path(),
69
+ "please repair all",
70
+ RouteOptions {
71
+ execute: true,
72
+ evaluation: EvaluationOptions::offline(),
73
+ },
74
+ )
75
+ .unwrap();
76
+
77
+ assert_eq!(route.policy_action, "auto_commit_harness_refresh_baseline");
78
+ assert!(route.mutation_performed);
79
+ assert_eq!(
80
+ route.executed_actions,
81
+ vec!["commit_harness_refresh_baseline".to_string()]
82
+ );
83
+ assert_eq!(repo.git_status_short(), "M USER.md");
84
+
85
+ assert_commit_paths(&repo, &["AGENTS.md", ".naome/manifest.json"], &["USER.md"]);
86
+ }
87
+
88
+ #[test]
89
+ fn dry_route_plans_harness_refresh_split_before_completed_task_baseline() {
90
+ let repo = TestRepo::completed_task_with_harness_refresh_diff("route-dry-harness-refresh");
91
+ let before = repo.git_stdout(&["rev-parse", "HEAD"]);
92
+
93
+ let route = route_new_task(&repo, "Start a new task for README polish.", false);
94
+
95
+ assert_eq!(
96
+ route.policy_action,
97
+ "auto_commit_harness_refresh_then_completed_task_then_create_new_task"
98
+ );
99
+ assert!(!route.mutation_performed);
100
+ assert!(!route.can_create_task);
101
+ assert!(route.human_options.is_empty());
102
+ assert!(route.intent.risk_codes.is_empty());
103
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
104
+ }
105
+
106
+ #[test]
107
+ fn execute_route_splits_harness_refresh_then_completed_task_baseline() {
108
+ let repo = TestRepo::completed_task_with_harness_refresh_diff("route-execute-harness-refresh");
109
+
110
+ let route = route_new_task(&repo, "Start a new task for README polish.", true);
111
+
112
+ assert_eq!(
113
+ route.policy_action,
114
+ "auto_commit_harness_refresh_then_completed_task_then_create_new_task"
115
+ );
116
+ assert!(route.mutation_performed);
117
+ assert!(route.can_create_task);
118
+ assert_eq!(
119
+ route.executed_actions,
120
+ vec![
121
+ "commit_harness_refresh_baseline".to_string(),
122
+ "commit_task_baseline".to_string()
123
+ ]
124
+ );
125
+ assert_eq!(route.next_decision.state, "ready_for_task");
126
+ assert!(repo.git_status_short().is_empty());
127
+
128
+ let log = repo.git_stdout(&["log", "--format=%s", "-2"]);
129
+ assert!(log.contains("chore(naome): baseline completed task"));
130
+ assert!(log.contains("chore(naome): baseline harness refresh"));
131
+
132
+ let journal = fs::read_to_string(repo.path().join(".naome/task-journal.jsonl")).unwrap();
133
+ assert!(journal.contains("\"taskId\":\"readme-task\""));
134
+ assert!(journal.contains("\"outcome\":\"route_auto_baseline\""));
135
+ }
@@ -0,0 +1,198 @@
1
+ use std::fs;
2
+
3
+ use naome_core::{evaluate_route, EvaluationOptions, RouteOptions};
4
+ use serde_json::json;
5
+
6
+ mod repo_support;
7
+
8
+ use repo_support::{assert_user_diff_committed, route_commit_request, TestRepo};
9
+
10
+ #[test]
11
+ fn execute_route_does_not_mutate_or_offer_clear_commit_for_dirty_unowned_diff() {
12
+ let repo = TestRepo::idle_readme("route-dirty-commit-request");
13
+ repo.write_file("README.md", "# Manual edit\n");
14
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
15
+ let before_status = repo.git_status_short();
16
+
17
+ let route = evaluate_route(
18
+ repo.path(),
19
+ "clear_or_commit_unowned_diff",
20
+ RouteOptions {
21
+ execute: true,
22
+ evaluation: EvaluationOptions::offline(),
23
+ },
24
+ )
25
+ .unwrap();
26
+
27
+ assert_eq!(route.policy_action, "block_unowned_diff");
28
+ assert!(!route.mutation_performed);
29
+ assert!(!route.can_create_task);
30
+ assert_eq!(route.executed_actions, Vec::<String>::new());
31
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
32
+ assert_eq!(repo.git_status_short(), before_status);
33
+ assert!(!route
34
+ .human_options
35
+ .contains(&"clear_or_commit_unowned_diff".to_string()));
36
+ }
37
+
38
+ #[test]
39
+ fn execute_route_commits_user_diff_after_quality_gate_passes() {
40
+ let repo = TestRepo::readme_quality_repo(
41
+ "route-user-diff-quality-pass",
42
+ Vec::new(),
43
+ vec!["diff-check"],
44
+ );
45
+ repo.write_file("README.md", "# Manual edit\n");
46
+
47
+ let route = route_commit_request(&repo);
48
+
49
+ assert_user_diff_committed(&route, &repo);
50
+
51
+ let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
52
+ assert!(committed_paths.contains("README.md"));
53
+ assert!(route.user_message.contains("quality gates passed"));
54
+ }
55
+
56
+ #[test]
57
+ fn execute_route_runs_repository_quality_check_without_shelling_to_repo_command() {
58
+ let repo = TestRepo::repository_quality_failure_repo("route-repository-quality-check");
59
+ repo.write_file(
60
+ "src/large.js",
61
+ "export function legacy() {\n one();\n two();\n three();\n four();\n five();\n}\n",
62
+ );
63
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
64
+
65
+ let route = route_commit_request(&repo);
66
+
67
+ assert_quality_gate_blocked(&repo, &route, before_head, "src/large.js");
68
+ assert!(route.user_message.contains("repository-quality-check"));
69
+ assert!(route.user_message.contains("file-length"));
70
+ }
71
+
72
+ #[test]
73
+ fn execute_route_commits_product_diff_with_declared_safe_quality_checks() {
74
+ let repo = TestRepo::product_quality_repo("route-product-diff-quality-pass");
75
+ repo.write_file(
76
+ "packages/naome/crates/naome-core/src/route.rs",
77
+ "pub fn changed() {}\n",
78
+ );
79
+
80
+ let route = route_commit_request(&repo);
81
+
82
+ assert_user_diff_committed(&route, &repo);
83
+
84
+ let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
85
+ assert!(committed_paths.contains("packages/naome/crates/naome-core/src/route.rs"));
86
+ assert!(route.user_message.contains("quality gates passed"));
87
+ }
88
+
89
+ #[test]
90
+ fn execute_route_preserves_staged_rename_when_committing_user_diff() {
91
+ let repo = TestRepo::new("route-user-diff-staged-rename");
92
+ repo.init_git();
93
+ repo.write_file("README.md", "# Baseline\n");
94
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
95
+ repo.write_readme_rename_verification();
96
+ repo.commit_all("baseline");
97
+ repo.git(&["mv", "README.md", "INTRO.md"]);
98
+
99
+ let route = route_commit_request(&repo);
100
+
101
+ assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
102
+ assert!(route.allowed);
103
+ assert!(route.mutation_performed);
104
+ assert_eq!(repo.git_status_short(), "");
105
+
106
+ let committed_paths = repo.git_stdout(&["show", "--name-status", "--format=", "HEAD"]);
107
+ assert!(committed_paths.contains("README.md"));
108
+ assert!(committed_paths.contains("INTRO.md"));
109
+ }
110
+
111
+ #[test]
112
+ fn execute_route_refuses_user_diff_commit_without_quality_coverage() {
113
+ let repo = TestRepo::idle_readme("route-user-diff-no-quality-coverage");
114
+ repo.write_file("README.md", "# Manual edit\n");
115
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
116
+
117
+ let route = route_commit_request(&repo);
118
+
119
+ assert_quality_gate_blocked(&repo, &route, before_head, "README.md");
120
+ assert!(route.user_message.contains("No quality coverage"));
121
+ }
122
+
123
+ #[test]
124
+ fn execute_route_refuses_user_diff_commit_when_check_mutates_after_diff_check() {
125
+ let repo = TestRepo::readme_quality_repo(
126
+ "route-user-diff-mutating-check",
127
+ vec![json!({
128
+ "id": "mutate-readme-after-check",
129
+ "command": "node -e \"require('fs').writeFileSync('README.md', '# Mutated \\\\n')\"",
130
+ "cwd": ".",
131
+ "purpose": "Simulate a mutating check that dirties checked content.",
132
+ "cost": "fast",
133
+ "source": "test",
134
+ "evidence": ["README.md"],
135
+ "lastVerified": null
136
+ })],
137
+ vec!["mutate-readme-after-check"],
138
+ );
139
+ repo.write_file("README.md", "# Manual edit\n");
140
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
141
+
142
+ let route = route_commit_request(&repo);
143
+
144
+ assert_quality_gate_blocked(&repo, &route, before_head, "README.md");
145
+ assert_eq!(
146
+ fs::read_to_string(repo.path().join("README.md")).unwrap(),
147
+ "# Manual edit\n"
148
+ );
149
+ assert!(route
150
+ .user_message
151
+ .contains("will not execute repository-controlled verification commands"));
152
+ }
153
+
154
+ #[test]
155
+ fn execute_route_refuses_user_diff_commit_when_diff_check_command_is_unsafe() {
156
+ let repo = TestRepo::readme_unsafe_diff_check_repo("route-user-diff-mutating-diff-check");
157
+ repo.write_file("README.md", "# Manual edit\n");
158
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
159
+
160
+ let route = route_commit_request(&repo);
161
+
162
+ assert_quality_gate_blocked(&repo, &route, before_head, "README.md");
163
+ assert!(!repo.path().join("NEW.md").exists());
164
+ assert!(route
165
+ .user_message
166
+ .contains("Quality check diff-check has an unsafe command or cwd"));
167
+ }
168
+
169
+ #[test]
170
+ fn execute_route_refuses_user_diff_commit_when_quality_gate_fails() {
171
+ let repo = TestRepo::readme_quality_repo(
172
+ "route-user-diff-quality-fail",
173
+ Vec::new(),
174
+ vec!["diff-check"],
175
+ );
176
+ repo.write_file("README.md", "# Manual edit \n");
177
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
178
+
179
+ let route = route_commit_request(&repo);
180
+
181
+ assert_quality_gate_blocked(&repo, &route, before_head, "README.md");
182
+ assert_eq!(route.human_options, vec!["review_unowned_diff"]);
183
+ assert!(route.user_message.contains("quality gate failed"));
184
+ }
185
+
186
+ fn assert_quality_gate_blocked(
187
+ repo: &TestRepo,
188
+ route: &naome_core::RouteDecision,
189
+ before_head: String,
190
+ changed_path: &str,
191
+ ) {
192
+ assert_eq!(route.policy_action, "commit_user_diff_with_quality_gate");
193
+ assert!(!route.allowed);
194
+ assert!(!route.mutation_performed);
195
+ assert_eq!(route.executed_actions, vec!["run_user_diff_quality_gate"]);
196
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
197
+ assert!(repo.git_status_short().contains(changed_path));
198
+ }
@@ -0,0 +1,54 @@
1
+ use serde_json::json;
2
+
3
+ mod repo_support;
4
+
5
+ use repo_support::{route_readme_task, try_route_readme_task, TestRepo};
6
+
7
+ #[test]
8
+ fn execute_route_refuses_to_create_more_than_max_isolated_worktrees() {
9
+ let repo = TestRepo::new("route-worktree-limit");
10
+ repo.init_git();
11
+ repo.write_file("README.md", "# Baseline\n");
12
+ repo.write_file("USER.md", "user baseline\n");
13
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
14
+ repo.git(&["add", "."]);
15
+ repo.git(&["commit", "-m", "baseline"]);
16
+ repo.write_file("USER.md", "user local edit\n");
17
+ repo.create_stale_task_worktrees(25);
18
+
19
+ let error = try_route_readme_task(&repo, true).unwrap_err();
20
+
21
+ assert!(error
22
+ .to_string()
23
+ .contains("Too many NAOME task worktrees are present"));
24
+ }
25
+
26
+ #[test]
27
+ fn execute_route_preflights_worktree_before_completed_task_baseline() {
28
+ let repo =
29
+ TestRepo::completed_task_with_unrelated_user_edit("route-completed-worktree-preflight");
30
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
31
+ let before_status = repo.git_status_short();
32
+ repo.create_stale_task_worktrees(25);
33
+
34
+ let error = try_route_readme_task(&repo, true).unwrap_err();
35
+
36
+ assert!(error
37
+ .to_string()
38
+ .contains("Too many NAOME task worktrees are present"));
39
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before_head);
40
+ assert_eq!(repo.git_status_short(), before_status);
41
+ }
42
+
43
+ #[test]
44
+ fn execute_route_uses_preflighted_worktree_name_after_completed_task_baseline() {
45
+ let repo = TestRepo::completed_task_with_unrelated_user_edit("route-worktree-name-preflight");
46
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
47
+ let before_short = &before_head[..12];
48
+
49
+ let route = route_readme_task(&repo, true);
50
+
51
+ let worktree = route.worktree.expect("route should create a worktree");
52
+ assert!(worktree.branch.contains(before_short));
53
+ assert!(worktree.path.contains(before_short));
54
+ }