@lamentis/naome 1.3.7 → 1.3.8

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 (30) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +5 -0
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-core/Cargo.toml +1 -1
  5. package/crates/naome-core/src/context/select.rs +58 -4
  6. package/crates/naome-core/src/intent/classifier.rs +56 -81
  7. package/crates/naome-core/src/intent/envelope.rs +173 -19
  8. package/crates/naome-core/src/intent/legacy_response.rs +2 -0
  9. package/crates/naome-core/src/intent/model.rs +6 -0
  10. package/crates/naome-core/src/intent/resolver.rs +25 -0
  11. package/crates/naome-core/src/intent/risk.rs +11 -1
  12. package/crates/naome-core/src/intent.rs +1 -1
  13. package/crates/naome-core/src/route/context.rs +8 -0
  14. package/crates/naome-core/tests/context.rs +92 -0
  15. package/crates/naome-core/tests/intent.rs +98 -18
  16. package/crates/naome-core/tests/intent_support/mod.rs +39 -1
  17. package/crates/naome-core/tests/intent_v2.rs +299 -10
  18. package/crates/naome-core/tests/repo_support/routes.rs +8 -2
  19. package/crates/naome-core/tests/route_baseline.rs +29 -0
  20. package/crates/naome-core/tests/route_completion.rs +26 -5
  21. package/crates/naome-core/tests/route_harness_refresh.rs +7 -1
  22. package/crates/naome-core/tests/route_user_diff.rs +1 -1
  23. package/crates/naome-core/tests/task_state_compact.rs +7 -1
  24. package/native/darwin-arm64/naome +0 -0
  25. package/native/linux-x64/naome +0 -0
  26. package/package.json +1 -1
  27. package/templates/naome-root/.naome/manifest.json +2 -2
  28. package/templates/naome-root/docs/naome/agent-workflow.md +14 -5
  29. package/templates/naome-root/docs/naome/architecture.md +9 -0
  30. package/crates/naome-core/src/intent/patterns.rs +0 -170
@@ -89,6 +89,20 @@ fn resolve_policy(
89
89
  } else if prompt_intent == IntentKind::Unsafe {
90
90
  reason_codes.push("winning_rule:unsafe_intent_precedence".to_string());
91
91
  unsafe_policy()
92
+ } else if prompt_intent == IntentKind::PromptNormalizationRequired {
93
+ (
94
+ "normalize_prompt_first",
95
+ true,
96
+ "NAOME needs a structured prompt envelope before routing or mutating repository state.",
97
+ "Normalize the user prompt into the deterministic NAOME prompt envelope, then rerun routing.",
98
+ )
99
+ } else if prompt_intent == IntentKind::Advisory {
100
+ (
101
+ "answer_without_mutation",
102
+ true,
103
+ "The prompt envelope classifies this as advisory or planning-only work.",
104
+ "Answer without creating a task, journaling, editing files, committing, pushing, or creating a merge request.",
105
+ )
92
106
  } else if prompt_intent == IntentKind::StatusQuestion {
93
107
  (
94
108
  "answer_status_only",
@@ -113,11 +127,20 @@ fn reason_codes(
113
127
  prompt_intent: IntentKind,
114
128
  ) -> Vec<String> {
115
129
  let mut codes = vec![format!("repo_state:{}", decision.state)];
130
+ if canonical.has_routing_envelope {
131
+ codes.push("prompt_has_routing_envelope".to_string());
132
+ }
133
+ codes.extend(canonical.validation_errors.iter().cloned());
116
134
  if canonical.references_current_task {
117
135
  codes.push("prompt_references_current_task".to_string());
118
136
  }
119
137
  for (kind, code) in [
120
138
  (IntentKind::NewTask, "prompt_requests_new_goal"),
139
+ (
140
+ IntentKind::PromptNormalizationRequired,
141
+ "prompt_requires_normalization_envelope",
142
+ ),
143
+ (IntentKind::Advisory, "prompt_requests_advisory_answer"),
121
144
  (IntentKind::TaskRevision, "prompt_requests_correction"),
122
145
  (IntentKind::CommitRequest, "prompt_requests_commit"),
123
146
  (IntentKind::NoCommitRequest, "prompt_blocks_commit"),
@@ -153,6 +176,8 @@ fn certainty(intent: IntentKind, policy_action: &str, risk_codes: &[String]) ->
153
176
  | IntentKind::RepairRequest
154
177
  | IntentKind::NoCommitRequest
155
178
  | IntentKind::CancelRequest
179
+ | IntentKind::PromptNormalizationRequired
180
+ | IntentKind::Advisory
156
181
  ) {
157
182
  "exact"
158
183
  } else {
@@ -1,5 +1,4 @@
1
1
  use super::model::{PromptSegment, RiskContext, SegmentKind};
2
- use super::patterns::lexical_tokens;
3
2
 
4
3
  pub(crate) fn detect_risk(segments: &[PromptSegment]) -> RiskContext {
5
4
  let mut codes = Vec::new();
@@ -38,3 +37,14 @@ fn is_publish_command(tokens: &[String]) -> bool {
38
37
  .windows(2)
39
38
  .any(|window| window[0] == "npm" && window[1] == "publish")
40
39
  }
40
+
41
+ fn lexical_tokens(text: &str) -> Vec<String> {
42
+ text.split_whitespace()
43
+ .map(|token| {
44
+ token
45
+ .trim_matches(|ch: char| !ch.is_alphanumeric() && ch != '_' && ch != '-')
46
+ .to_lowercase()
47
+ })
48
+ .filter(|token| !token.is_empty())
49
+ .collect()
50
+ }
@@ -3,7 +3,6 @@ mod envelope;
3
3
  mod legacy;
4
4
  mod legacy_response;
5
5
  mod model;
6
- mod patterns;
7
6
  mod resolver;
8
7
  mod resolver_active;
9
8
  mod resolver_baseline;
@@ -23,6 +22,7 @@ use crate::task_state::{
23
22
  };
24
23
 
25
24
  use classifier::canonical_intent;
25
+ pub(crate) use envelope::prompt_envelope_json;
26
26
  pub use legacy::{format_intent, IntentDecision, PromptEvidence};
27
27
  use resolver::{resolve_intent, CompletedTaskReadiness, DirtyDiffReadiness};
28
28
 
@@ -57,6 +57,8 @@ pub(super) fn winning_rule(intent: &IntentDecision) -> String {
57
57
  "current_task_revision_continues_task"
58
58
  }
59
59
  "answer_status_only" => "status_request_read_only",
60
+ "normalize_prompt_first" => "prompt_envelope_required_before_routing",
61
+ "answer_without_mutation" => "advisory_prompt_read_only",
60
62
  "create_new_task" | "create_new_task_without_auto_baseline" => "ready_repo_new_task",
61
63
  "create_isolated_task_worktree" => "dirty_repo_new_task_worktree_isolation",
62
64
  "commit_user_diff_with_quality_gate" => "explicit_user_diff_commit_quality_gate",
@@ -164,6 +166,12 @@ pub(super) fn required_context_for_intent(intent: &IntentDecision) -> Vec<String
164
166
  "answer_status_only" => {
165
167
  push_unique(&mut context, "docs/naome/index.md");
166
168
  }
169
+ "normalize_prompt_first" => {
170
+ push_unique(&mut context, "docs/naome/agent-workflow.md");
171
+ }
172
+ "answer_without_mutation" => {
173
+ push_unique(&mut context, "docs/naome/index.md");
174
+ }
167
175
  "repair_harness_only" => {
168
176
  push_unique(&mut context, ".naome/manifest.json");
169
177
  push_unique(&mut context, "docs/naome/index.md");
@@ -66,6 +66,91 @@ fn prompt_context_uses_file_mentions_without_broad_markdown_context() {
66
66
  .any(|item| item.path == "docs/naome/repository-quality.md"));
67
67
  }
68
68
 
69
+ #[test]
70
+ fn prompt_context_ignores_nonexistent_concept_tokens_that_look_path_like() {
71
+ let repo = context_repo("context-prompt-concept-terms");
72
+ repo.write_file("docs/naome/repository-quality.md", "# Quality\n");
73
+ repo.commit_all("baseline");
74
+
75
+ let selection = select_context_for_prompt(
76
+ repo.path(),
77
+ "Advisory/planning-only prompts and German/English examples must not become paths.",
78
+ )
79
+ .unwrap();
80
+
81
+ assert_eq!(selection.mode, "prompt");
82
+ assert_eq!(
83
+ selection.required_context[0].path,
84
+ ".naome/verification.json"
85
+ );
86
+ assert!(!selection
87
+ .required_context
88
+ .iter()
89
+ .any(|item| item.path == "Advisory/planning-only" || item.path == "German/English"));
90
+ }
91
+
92
+ #[test]
93
+ fn prompt_context_prefers_envelope_referenced_paths_over_raw_prompt_tokens() {
94
+ let repo = context_repo("context-prompt-envelope-paths");
95
+ repo.write_file("packages/app/src/lib.rs", "pub fn app() {}\n");
96
+ repo.write_file("packages/app/src/other.rs", "pub fn other() {}\n");
97
+ repo.commit_all("baseline");
98
+
99
+ let selection = select_context_for_prompt(
100
+ repo.path(),
101
+ "```naome-prompt-envelope-v1\n{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[\"packages/app/src/lib.rs\"],\"constraints\":[],\"uncertainties\":[]}\n```\n\nPlease mention packages/app/src/other.rs in prose but use the envelope path.",
102
+ )
103
+ .unwrap();
104
+
105
+ assert_eq!(selection.mode, "prompt");
106
+ assert_eq!(
107
+ selection.required_context[0].path,
108
+ "packages/app/src/lib.rs"
109
+ );
110
+ assert!(!selection
111
+ .required_context
112
+ .iter()
113
+ .any(|item| item.path == "packages/app/src/other.rs"));
114
+ }
115
+
116
+ #[test]
117
+ fn prompt_context_keeps_envelope_paths_for_nested_creation_targets() {
118
+ let repo = app_context_repo("context-prompt-envelope-new-paths");
119
+
120
+ let selection = select_context_for_prompt(
121
+ repo.path(),
122
+ "```naome-prompt-envelope-v1\n{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"create_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[\"packages/app/src/new/mod.rs\"],\"constraints\":[],\"uncertainties\":[]}\n```\n\nCreate the new module.",
123
+ )
124
+ .unwrap();
125
+
126
+ assert_eq!(selection.mode, "prompt");
127
+ assert_eq!(
128
+ selection.required_context[0].path,
129
+ "packages/app/src/new/mod.rs"
130
+ );
131
+ }
132
+
133
+ #[test]
134
+ fn prompt_context_rejects_envelope_paths_outside_repository() {
135
+ let repo = app_context_repo("context-prompt-envelope-safe-paths");
136
+
137
+ let selection = select_context_for_prompt(
138
+ repo.path(),
139
+ "```naome-prompt-envelope-v1\n{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[\"../notes.md\",\"/tmp/file.rs\",\"packages/app/src/lib.rs\"],\"constraints\":[],\"uncertainties\":[]}\n```",
140
+ )
141
+ .unwrap();
142
+
143
+ assert_eq!(selection.mode, "prompt");
144
+ assert_eq!(
145
+ selection.required_context[0].path,
146
+ "packages/app/src/lib.rs"
147
+ );
148
+ assert!(!selection
149
+ .required_context
150
+ .iter()
151
+ .any(|item| item.path == "../notes.md" || item.path == "/tmp/file.rs"));
152
+ }
153
+
69
154
  #[test]
70
155
  fn context_selection_reports_over_budget_when_many_paths_change() {
71
156
  let repo = context_repo("context-budget");
@@ -97,3 +182,10 @@ fn context_repo(name: &str) -> TestRepo {
97
182
  );
98
183
  repo
99
184
  }
185
+
186
+ fn app_context_repo(name: &str) -> TestRepo {
187
+ let repo = context_repo(name);
188
+ repo.write_file("packages/app/src/lib.rs", "pub fn app() {}\n");
189
+ repo.commit_all("baseline");
190
+ repo
191
+ }
@@ -2,13 +2,20 @@ mod intent_support;
2
2
 
3
3
  use naome_core::IntentDecision;
4
4
 
5
- use intent_support::{completed_state, env, idle, Repo};
5
+ use intent_support::{completed_state, idle, prompt_env, prompt_env_with_actions, Repo};
6
6
 
7
7
  #[test]
8
8
  fn repo_state_and_envelope_resolve_task_policy() {
9
9
  let clean = Repo::clean("clean", idle());
10
10
  assert_intent(
11
- &clean.intent("Please implement a small new feature."),
11
+ &clean.intent(&prompt_env(
12
+ "Please implement a small new feature.",
13
+ "implementation",
14
+ "modify_files",
15
+ "none",
16
+ "new_task",
17
+ "none",
18
+ )),
12
19
  "ready_for_task",
13
20
  "new_task",
14
21
  "create_new_task",
@@ -18,7 +25,14 @@ fn repo_state_and_envelope_resolve_task_policy() {
18
25
  let mut active_state = completed_state("HEAD", true);
19
26
  active_state["status"] = serde_json::json!("implementing");
20
27
  let active = Repo::clean("active", active_state);
21
- let revision = active.intent(&env("follow up", "none", "task_revision", "none"));
28
+ let revision = active.intent(&prompt_env(
29
+ "follow up",
30
+ "revision",
31
+ "modify_files",
32
+ "none",
33
+ "task_revision",
34
+ "none",
35
+ ));
22
36
  assert_intent(
23
37
  &revision,
24
38
  "active_task_in_progress",
@@ -26,7 +40,14 @@ fn repo_state_and_envelope_resolve_task_policy() {
26
40
  "continue_current_task",
27
41
  true,
28
42
  );
29
- let distinct = active.intent(&env("separate work", "none", "new_task", "none"));
43
+ let distinct = active.intent(&prompt_env(
44
+ "separate work",
45
+ "implementation",
46
+ "modify_files",
47
+ "none",
48
+ "new_task",
49
+ "none",
50
+ ));
30
51
  assert_intent(
31
52
  &distinct,
32
53
  "active_task_in_progress",
@@ -36,7 +57,14 @@ fn repo_state_and_envelope_resolve_task_policy() {
36
57
  );
37
58
 
38
59
  let completed = Repo::completed("completed", true);
39
- let next = completed.intent("After that, please implement a new task.");
60
+ let next = completed.intent(&prompt_env(
61
+ "After that, please implement a new task.",
62
+ "implementation",
63
+ "modify_files",
64
+ "none",
65
+ "new_task",
66
+ "none",
67
+ ));
40
68
  assert_intent(
41
69
  &next,
42
70
  "completed_task_unbaselined",
@@ -46,25 +74,55 @@ fn repo_state_and_envelope_resolve_task_policy() {
46
74
  );
47
75
  assert_eq!(
48
76
  completed
49
- .intent(&env("review", "review_request", "new_task", "none"))
77
+ .intent(&prompt_env_with_actions(
78
+ "review",
79
+ "implementation",
80
+ "none",
81
+ "review_request",
82
+ "none",
83
+ "none",
84
+ &["review"],
85
+ ))
50
86
  .policy_action,
51
87
  "review_task_diff"
52
88
  );
53
89
  assert_eq!(
54
90
  completed
55
- .intent(&env("cancel", "cancel_request", "new_task", "none"))
91
+ .intent(&prompt_env_with_actions(
92
+ "cancel",
93
+ "implementation",
94
+ "none",
95
+ "cancel_request",
96
+ "none",
97
+ "none",
98
+ &["cancel"],
99
+ ))
56
100
  .policy_action,
57
101
  "cancel_task_changes"
58
102
  );
59
103
  assert_eq!(
60
104
  completed
61
- .intent(&env("no commit", "no_commit_request", "new_task", "none"))
105
+ .intent(&prompt_env(
106
+ "no commit",
107
+ "implementation",
108
+ "none",
109
+ "no_commit_request",
110
+ "new_task",
111
+ "none",
112
+ ))
62
113
  .policy_action,
63
114
  "block_auto_baseline_due_to_no_commit"
64
115
  );
65
116
  assert_eq!(
66
117
  completed
67
- .intent(&env("revise", "none", "task_revision", "none"))
118
+ .intent(&prompt_env(
119
+ "revise",
120
+ "revision",
121
+ "modify_files",
122
+ "none",
123
+ "task_revision",
124
+ "none",
125
+ ))
68
126
  .policy_action,
69
127
  "reopen_completed_task_revision"
70
128
  );
@@ -72,8 +130,14 @@ fn repo_state_and_envelope_resolve_task_policy() {
72
130
 
73
131
  #[test]
74
132
  fn workflow_and_risk_intents_are_structured_not_language_keywords() {
75
- let status =
76
- Repo::clean("status", idle()).intent(&env("anything", "status_question", "none", "none"));
133
+ let status = Repo::clean("status", idle()).intent(&prompt_env(
134
+ "anything",
135
+ "status",
136
+ "none",
137
+ "status_question",
138
+ "none",
139
+ "none",
140
+ ));
77
141
  assert_intent(
78
142
  &status,
79
143
  "ready_for_task",
@@ -84,7 +148,14 @@ fn workflow_and_risk_intents_are_structured_not_language_keywords() {
84
148
 
85
149
  let dirty = Repo::clean("dirty", idle());
86
150
  dirty.write("README.md", "# Manual edit\n");
87
- let commit = dirty.intent(&env("okay commit it", "commit_request", "none", "none"));
151
+ let commit = dirty.intent(&prompt_env(
152
+ "okay commit it",
153
+ "implementation",
154
+ "commit",
155
+ "commit_request",
156
+ "none",
157
+ "none",
158
+ ));
88
159
  assert_intent(
89
160
  &commit,
90
161
  "dirty_unowned_diff",
@@ -97,15 +168,17 @@ fn workflow_and_risk_intents_are_structured_not_language_keywords() {
97
168
  assert_intent(
98
169
  &deprecated,
99
170
  "dirty_unowned_diff",
100
- "ambiguous",
101
- "block_unowned_diff",
102
- false,
171
+ "prompt_normalization_required",
172
+ "normalize_prompt_first",
173
+ true,
103
174
  );
104
175
 
105
- let risky = Repo::clean("risky", idle()).intent(&env(
176
+ let risky = Repo::clean("risky", idle()).intent(&prompt_env(
106
177
  "include this API key",
178
+ "implementation",
179
+ "commit",
107
180
  "commit_request",
108
- "new_task",
181
+ "none",
109
182
  "credential_context",
110
183
  ));
111
184
  assert_eq!(risky.prompt_intent, "unsafe");
@@ -116,7 +189,14 @@ fn workflow_and_risk_intents_are_structured_not_language_keywords() {
116
189
  #[test]
117
190
  fn invalid_completed_task_proof_blocks_new_task_baseline() {
118
191
  let repo = Repo::completed("invalid", false);
119
- let intent = repo.intent("After that, please implement a new task.");
192
+ let intent = repo.intent(&prompt_env(
193
+ "After that, please implement a new task.",
194
+ "implementation",
195
+ "modify_files",
196
+ "none",
197
+ "new_task",
198
+ "none",
199
+ ));
120
200
  assert_eq!(intent.policy_action, "block_unsafe_intent");
121
201
  assert!(intent
122
202
  .risk_codes
@@ -11,12 +11,50 @@ use serde_json::{json, Value};
11
11
 
12
12
  static REPO_COUNTER: AtomicU64 = AtomicU64::new(0);
13
13
 
14
- pub fn env(prompt: &str, workflow: &str, task: &str, risk: &str) -> String {
14
+ pub fn legacy_env(prompt: &str, workflow: &str, task: &str, risk: &str) -> String {
15
15
  format!(
16
16
  "```naome-intent-v2\n{{\"schema\":\"naome.intent.v2\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"{risk}\"}}\n```\n\n{prompt}"
17
17
  )
18
18
  }
19
19
 
20
+ pub fn prompt_env(
21
+ prompt: &str,
22
+ request_kind: &str,
23
+ mutation_intent: &str,
24
+ workflow: &str,
25
+ task: &str,
26
+ risk: &str,
27
+ ) -> String {
28
+ prompt_env_with_actions(
29
+ prompt,
30
+ request_kind,
31
+ mutation_intent,
32
+ workflow,
33
+ task,
34
+ risk,
35
+ &[],
36
+ )
37
+ }
38
+
39
+ pub fn prompt_env_with_actions(
40
+ prompt: &str,
41
+ request_kind: &str,
42
+ mutation_intent: &str,
43
+ workflow: &str,
44
+ task: &str,
45
+ risk: &str,
46
+ actions: &[&str],
47
+ ) -> String {
48
+ let requested_actions = actions
49
+ .iter()
50
+ .map(|action| format!("\"{action}\""))
51
+ .collect::<Vec<_>>()
52
+ .join(",");
53
+ format!(
54
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"{request_kind}\",\"mutationIntent\":\"{mutation_intent}\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"{risk}\",\"requestedActions\":[{requested_actions}],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
55
+ )
56
+ }
57
+
20
58
  pub fn idle() -> Value {
21
59
  json!({ "status": "idle", "activeTask": null })
22
60
  }