@lamentis/naome 1.3.7 → 1.3.9

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 (48) 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-cli/src/install_bridge.rs +56 -8
  5. package/crates/naome-core/Cargo.toml +1 -1
  6. package/crates/naome-core/src/context/select.rs +58 -4
  7. package/crates/naome-core/src/harness_health/integrity.rs +41 -23
  8. package/crates/naome-core/src/harness_health/manifest.rs +97 -0
  9. package/crates/naome-core/src/harness_health.rs +58 -106
  10. package/crates/naome-core/src/intent/classifier.rs +56 -81
  11. package/crates/naome-core/src/intent/envelope.rs +173 -19
  12. package/crates/naome-core/src/intent/legacy_response.rs +2 -0
  13. package/crates/naome-core/src/intent/model.rs +6 -0
  14. package/crates/naome-core/src/intent/resolver.rs +25 -0
  15. package/crates/naome-core/src/intent/risk.rs +11 -1
  16. package/crates/naome-core/src/intent.rs +1 -1
  17. package/crates/naome-core/src/quality/cache.rs +122 -19
  18. package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
  19. package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
  20. package/crates/naome-core/src/quality/scanner.rs +5 -2
  21. package/crates/naome-core/src/route/context.rs +8 -0
  22. package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
  23. package/crates/naome-core/tests/context.rs +92 -0
  24. package/crates/naome-core/tests/harness_health.rs +149 -0
  25. package/crates/naome-core/tests/intent.rs +98 -18
  26. package/crates/naome-core/tests/intent_support/mod.rs +39 -1
  27. package/crates/naome-core/tests/intent_v2.rs +299 -10
  28. package/crates/naome-core/tests/quality_performance.rs +63 -2
  29. package/crates/naome-core/tests/repo_support/routes.rs +8 -2
  30. package/crates/naome-core/tests/route_baseline.rs +29 -0
  31. package/crates/naome-core/tests/route_completion.rs +26 -5
  32. package/crates/naome-core/tests/route_harness_refresh.rs +7 -1
  33. package/crates/naome-core/tests/route_user_diff.rs +1 -1
  34. package/crates/naome-core/tests/task_state_compact.rs +7 -1
  35. package/installer/filesystem.js +38 -0
  36. package/installer/flows.js +6 -1
  37. package/installer/harness-file-ops.js +36 -8
  38. package/installer/manifest-state.js +2 -2
  39. package/installer/native.js +63 -18
  40. package/native/darwin-arm64/naome +0 -0
  41. package/native/linux-x64/naome +0 -0
  42. package/package.json +1 -1
  43. package/templates/naome-root/.naome/bin/check-harness-health.js +25 -21
  44. package/templates/naome-root/.naome/bin/check-task-state.js +35 -42
  45. package/templates/naome-root/.naome/manifest.json +10 -10
  46. package/templates/naome-root/docs/naome/agent-workflow.md +14 -5
  47. package/templates/naome-root/docs/naome/architecture.md +9 -0
  48. package/crates/naome-core/src/intent/patterns.rs +0 -170
@@ -1,29 +1,42 @@
1
1
  use std::collections::HashSet;
2
2
 
3
3
  use super::envelope::parse_routing_envelope;
4
- use super::model::{CanonicalIntent, IntentCandidate, IntentKind, PromptSegment, SegmentKind};
5
- use super::patterns;
4
+ use super::model::{CanonicalIntent, IntentCandidate, IntentKind};
6
5
  use super::risk::detect_risk;
7
6
  use super::segment::{actionable_text, segment_prompt};
8
7
 
9
- const DIRECT_WORKFLOW_CONFIDENCE: u8 = 90;
10
- const NEW_TASK_CONFIDENCE: u8 = 80;
11
- const REVISION_CONFIDENCE: u8 = 78;
12
- const GENERIC_WORK_CONFIDENCE: u8 = 50;
13
-
14
8
  pub(crate) fn canonical_intent(prompt: &str) -> CanonicalIntent {
15
9
  let segments = segment_prompt(prompt);
16
10
  let actionable = actionable_text(&segments);
17
- let mut candidates = classify_candidates(&segments);
11
+ let mut has_routing_envelope = false;
12
+ let mut validation_errors = Vec::new();
13
+ let mut candidates = Vec::new();
18
14
  let mut risk = detect_risk(&segments);
19
15
 
20
16
  if let Some(envelope) = parse_routing_envelope(prompt) {
17
+ has_routing_envelope = true;
21
18
  candidates.clear();
22
- candidates.extend(envelope.candidates);
19
+ validation_errors = envelope.validation_errors;
23
20
  risk.risk_codes.extend(envelope.risk.risk_codes);
24
21
  risk.risk_codes.sort();
25
22
  risk.risk_codes.dedup();
26
23
  risk.has_risky_terms = !risk.risk_codes.is_empty();
24
+ if validation_errors.is_empty() {
25
+ candidates.extend(envelope.candidates);
26
+ } else {
27
+ candidates.push(IntentCandidate {
28
+ kind: IntentKind::PromptNormalizationRequired,
29
+ confidence: 100,
30
+ evidence: "invalid_prompt_envelope".to_string(),
31
+ });
32
+ }
33
+ } else if !actionable.trim().is_empty() {
34
+ candidates.clear();
35
+ candidates.push(IntentCandidate {
36
+ kind: IntentKind::PromptNormalizationRequired,
37
+ confidence: 100,
38
+ evidence: "missing_prompt_envelope".to_string(),
39
+ });
27
40
  }
28
41
 
29
42
  candidates = dedupe_candidates(candidates);
@@ -37,11 +50,13 @@ pub(crate) fn canonical_intent(prompt: &str) -> CanonicalIntent {
37
50
  }
38
51
 
39
52
  CanonicalIntent {
40
- is_empty: actionable.trim().is_empty(),
53
+ is_empty: actionable.trim().is_empty() && !has_routing_envelope,
54
+ has_routing_envelope,
41
55
  references_current_task,
42
56
  has_workflow_conflict: has_workflow_conflict(&candidates),
43
57
  candidates,
44
58
  risk,
59
+ validation_errors,
45
60
  }
46
61
  }
47
62
 
@@ -51,14 +66,16 @@ pub(crate) fn winning_intent(canonical: &CanonicalIntent) -> IntentKind {
51
66
  }
52
67
  for kind in [
53
68
  IntentKind::Unsafe,
69
+ IntentKind::PromptNormalizationRequired,
54
70
  IntentKind::StatusQuestion,
71
+ IntentKind::Advisory,
55
72
  IntentKind::RepairRequest,
56
73
  IntentKind::CancelRequest,
57
74
  IntentKind::ReviewRequest,
58
75
  IntentKind::NoCommitRequest,
59
76
  IntentKind::CommitRequest,
60
- IntentKind::NewTask,
61
77
  IntentKind::TaskRevision,
78
+ IntentKind::NewTask,
62
79
  IntentKind::TaskCompletion,
63
80
  ] {
64
81
  if has_candidate(canonical, kind) {
@@ -75,67 +92,44 @@ pub(crate) fn has_candidate(canonical: &CanonicalIntent, kind: IntentKind) -> bo
75
92
  .any(|candidate| candidate.kind == kind)
76
93
  }
77
94
 
78
- fn classify_candidates(segments: &[PromptSegment]) -> Vec<IntentCandidate> {
79
- let mut candidates = Vec::new();
80
- for segment in action_segments(segments) {
81
- classify_direct_workflow(&mut candidates, segment);
82
- classify_task_work(&mut candidates, &segment.text);
83
- }
84
-
85
- if candidates.is_empty() {
86
- let actionable = actionable_text(segments);
87
- if patterns::is_generic_work_request(&actionable) {
88
- push(
89
- &mut candidates,
90
- IntentKind::NewTask,
91
- GENERIC_WORK_CONFIDENCE,
92
- "generic_work_request",
93
- );
94
- }
95
- }
96
-
97
- dedupe_candidates(candidates)
98
- }
99
-
100
- fn classify_direct_workflow(candidates: &mut Vec<IntentCandidate>, segment: &PromptSegment) {
101
- let intents = if segment.kind == SegmentKind::CommandLike {
102
- patterns::command_workflow_intents(&segment.text)
103
- } else {
104
- patterns::structured_workflow_intents(&segment.text)
105
- };
106
- for intent in intents {
107
- push(
108
- candidates,
109
- intent,
110
- DIRECT_WORKFLOW_CONFIDENCE,
111
- &patterns::normalized(&segment.text),
112
- );
113
- }
114
- }
115
-
116
- fn classify_task_work(candidates: &mut Vec<IntentCandidate>, text: &str) {
117
- if let Some(intent) = patterns::explicit_task_intent(text) {
118
- let confidence = if intent == IntentKind::NewTask {
119
- NEW_TASK_CONFIDENCE
120
- } else {
121
- REVISION_CONFIDENCE
122
- };
123
- push(candidates, intent, confidence, &patterns::normalized(text));
124
- }
125
- }
126
-
127
95
  fn has_candidate_kind(candidates: &[IntentCandidate], kind: IntentKind) -> bool {
128
96
  candidates.iter().any(|candidate| candidate.kind == kind)
129
97
  }
130
98
 
131
99
  fn has_workflow_conflict(candidates: &[IntentCandidate]) -> bool {
100
+ let kinds = candidates
101
+ .iter()
102
+ .map(|candidate| candidate.kind)
103
+ .collect::<HashSet<_>>();
104
+ let mutating_kinds = [
105
+ IntentKind::NewTask,
106
+ IntentKind::TaskRevision,
107
+ IntentKind::CommitRequest,
108
+ IntentKind::RepairRequest,
109
+ IntentKind::ReviewRequest,
110
+ IntentKind::CancelRequest,
111
+ IntentKind::TaskCompletion,
112
+ ];
113
+ if (kinds.contains(&IntentKind::Advisory) || kinds.contains(&IntentKind::StatusQuestion))
114
+ && mutating_kinds.iter().any(|kind| kinds.contains(kind))
115
+ {
116
+ return true;
117
+ }
118
+ if kinds.contains(&IntentKind::CommitRequest) && kinds.contains(&IntentKind::NoCommitRequest) {
119
+ return true;
120
+ }
121
+
132
122
  let workflow = candidates
133
123
  .iter()
134
- .filter(|candidate| candidate.confidence >= DIRECT_WORKFLOW_CONFIDENCE)
124
+ .filter(|candidate| candidate.confidence >= 90)
135
125
  .filter(|candidate| {
136
126
  !matches!(
137
127
  candidate.kind,
138
- IntentKind::NewTask | IntentKind::TaskRevision | IntentKind::Unsafe
128
+ IntentKind::NewTask
129
+ | IntentKind::TaskRevision
130
+ | IntentKind::Unsafe
131
+ | IntentKind::Advisory
132
+ | IntentKind::StatusQuestion
139
133
  )
140
134
  })
141
135
  .map(|candidate| candidate.kind)
@@ -143,25 +137,6 @@ fn has_workflow_conflict(candidates: &[IntentCandidate]) -> bool {
143
137
  workflow.len() > 1
144
138
  }
145
139
 
146
- fn action_segments(segments: &[PromptSegment]) -> impl Iterator<Item = &PromptSegment> {
147
- segments.iter().filter(|segment| {
148
- matches!(
149
- segment.kind,
150
- super::model::SegmentKind::Prose
151
- | super::model::SegmentKind::ListItem
152
- | super::model::SegmentKind::CommandLike
153
- )
154
- })
155
- }
156
-
157
- fn push(candidates: &mut Vec<IntentCandidate>, kind: IntentKind, confidence: u8, text: &str) {
158
- candidates.push(IntentCandidate {
159
- kind,
160
- confidence,
161
- evidence: text.to_string(),
162
- });
163
- }
164
-
165
140
  fn dedupe_candidates(candidates: Vec<IntentCandidate>) -> Vec<IntentCandidate> {
166
141
  let mut seen = HashSet::new();
167
142
  candidates
@@ -1,19 +1,25 @@
1
1
  use serde::Deserialize;
2
+ use serde_json::Value;
2
3
 
3
4
  use super::model::{IntentCandidate, IntentKind, RiskContext};
4
5
 
5
- const ENVELOPE_FENCE: &str = "```naome-intent-v2";
6
+ const PROMPT_ENVELOPE_FENCE: &str = "```naome-prompt-envelope-v1";
6
7
 
7
8
  #[derive(Debug, Clone, PartialEq, Eq)]
8
9
  pub(crate) struct RoutingEnvelope {
9
10
  pub candidates: Vec<IntentCandidate>,
10
11
  pub risk: RiskContext,
12
+ pub validation_errors: Vec<String>,
11
13
  }
12
14
 
13
15
  #[derive(Debug, Deserialize)]
14
16
  #[serde(rename_all = "camelCase")]
15
17
  struct EnvelopeInput {
16
18
  schema: Option<String>,
19
+ request_kind: Option<String>,
20
+ mutation_intent: Option<String>,
21
+ publication_intent: Option<String>,
22
+ requested_actions: Option<Vec<String>>,
17
23
  workflow_action: Option<String>,
18
24
  task_intent: Option<String>,
19
25
  risk: Option<String>,
@@ -21,32 +27,84 @@ struct EnvelopeInput {
21
27
  }
22
28
 
23
29
  pub(crate) fn parse_routing_envelope(prompt: &str) -> Option<RoutingEnvelope> {
24
- let json = envelope_json(prompt)?;
25
- let input = serde_json::from_str::<EnvelopeInput>(json).ok()?;
26
- if !matches!(input.schema.as_deref(), Some("naome.intent.v2")) {
27
- return None;
28
- }
30
+ let json = prompt_envelope_json(prompt)?;
31
+ let raw = match serde_json::from_str::<Value>(json) {
32
+ Ok(raw) => raw,
33
+ Err(_) => {
34
+ return Some(invalid_envelope(
35
+ vec!["prompt_envelope_invalid:json".to_string()],
36
+ RiskContext {
37
+ has_risky_terms: false,
38
+ risk_codes: Vec::new(),
39
+ },
40
+ ));
41
+ }
42
+ };
43
+ let input = match serde_json::from_value::<EnvelopeInput>(raw.clone()) {
44
+ Ok(input) => input,
45
+ Err(_) => {
46
+ let mut validation_errors = vec!["prompt_envelope_invalid:shape".to_string()];
47
+ let risk_codes = envelope_risk_codes(
48
+ raw.get("risk").and_then(Value::as_str),
49
+ raw_string_array(&raw, "riskCodes"),
50
+ &mut validation_errors,
51
+ );
52
+ return Some(invalid_envelope(
53
+ validation_errors,
54
+ RiskContext {
55
+ has_risky_terms: !risk_codes.is_empty(),
56
+ risk_codes,
57
+ },
58
+ ));
59
+ }
60
+ };
29
61
 
30
62
  let mut candidates = Vec::new();
31
- if let Some(kind) = workflow_action(input.workflow_action.as_deref()) {
63
+ let mut validation_errors = Vec::new();
64
+ if input.schema.as_deref() != Some("naome.prompt-envelope.v1") {
65
+ validation_errors.push("prompt_envelope_invalid:schema".to_string());
66
+ }
67
+ if let Some(kind) = request_kind(input.request_kind.as_deref(), &mut validation_errors) {
68
+ candidates.push(candidate(kind, "envelope.requestKind"));
69
+ }
70
+ if let Some(kind) = mutation_intent(input.mutation_intent.as_deref(), &mut validation_errors) {
71
+ candidates.push(candidate(kind, "envelope.mutationIntent"));
72
+ }
73
+ if let Some(kind) =
74
+ publication_intent(input.publication_intent.as_deref(), &mut validation_errors)
75
+ {
76
+ candidates.push(candidate(kind, "envelope.publicationIntent"));
77
+ }
78
+ for kind in requested_actions(input.requested_actions, &mut validation_errors) {
79
+ candidates.push(candidate(kind, "envelope.requestedActions"));
80
+ }
81
+ if let Some(kind) = workflow_action(input.workflow_action.as_deref(), &mut validation_errors) {
32
82
  candidates.push(candidate(kind, "envelope.workflowAction"));
33
83
  }
34
- if let Some(kind) = task_intent(input.task_intent.as_deref()) {
84
+ if let Some(kind) = task_intent(input.task_intent.as_deref(), &mut validation_errors) {
35
85
  candidates.push(candidate(kind, "envelope.taskIntent"));
36
86
  }
37
87
 
38
- let risk_codes = envelope_risk_codes(input.risk.as_deref(), input.risk_codes);
88
+ let risk_codes = envelope_risk_codes(
89
+ input.risk.as_deref(),
90
+ input.risk_codes,
91
+ &mut validation_errors,
92
+ );
39
93
  let risk = RiskContext {
40
94
  has_risky_terms: !risk_codes.is_empty(),
41
95
  risk_codes,
42
96
  };
43
97
 
44
- Some(RoutingEnvelope { candidates, risk })
98
+ Some(RoutingEnvelope {
99
+ candidates,
100
+ risk,
101
+ validation_errors,
102
+ })
45
103
  }
46
104
 
47
- fn envelope_json(prompt: &str) -> Option<&str> {
48
- let start = prompt.find(ENVELOPE_FENCE)?;
49
- let after_open = &prompt[start + ENVELOPE_FENCE.len()..];
105
+ pub(crate) fn prompt_envelope_json(prompt: &str) -> Option<&str> {
106
+ let fence = PROMPT_ENVELOPE_FENCE;
107
+ let after_open = prompt.strip_prefix(fence)?;
50
108
  let after_line = after_open
51
109
  .strip_prefix('\n')
52
110
  .or_else(|| after_open.strip_prefix("\r\n"))?;
@@ -54,7 +112,69 @@ fn envelope_json(prompt: &str) -> Option<&str> {
54
112
  Some(after_line[..end].trim())
55
113
  }
56
114
 
57
- fn workflow_action(value: Option<&str>) -> Option<IntentKind> {
115
+ fn request_kind(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
116
+ match normalized_value(value)?.as_str() {
117
+ "none" | "unknown" => None,
118
+ "advisory" | "planning" | "planning_only" | "question" | "recommendation" => {
119
+ Some(IntentKind::Advisory)
120
+ }
121
+ "implementation" | "execution" | "task" | "feature" | "fix" => Some(IntentKind::NewTask),
122
+ "revision" | "correction" => Some(IntentKind::TaskRevision),
123
+ "status" => Some(IntentKind::StatusQuestion),
124
+ _ => invalid_value("requestKind", errors),
125
+ }
126
+ }
127
+
128
+ fn mutation_intent(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
129
+ match normalized_value(value)?.as_str() {
130
+ "none" | "unknown" => None,
131
+ "modify_files" | "edit_files" | "create_files" | "delete_files" => {
132
+ Some(IntentKind::NewTask)
133
+ }
134
+ "commit" | "baseline" => Some(IntentKind::CommitRequest),
135
+ "publish" | "push" | "merge_request" | "pull_request" => {
136
+ unsupported_value("mutationIntent", errors)
137
+ }
138
+ _ => invalid_value("mutationIntent", errors),
139
+ }
140
+ }
141
+
142
+ fn publication_intent(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
143
+ match normalized_value(value)?.as_str() {
144
+ "none" | "unknown" => None,
145
+ "commit" => Some(IntentKind::CommitRequest),
146
+ "push" | "merge_request" | "pull_request" | "mr" | "pr" => {
147
+ unsupported_value("publicationIntent", errors)
148
+ }
149
+ _ => invalid_value("publicationIntent", errors),
150
+ }
151
+ }
152
+
153
+ fn requested_actions(values: Option<Vec<String>>, errors: &mut Vec<String>) -> Vec<IntentKind> {
154
+ values
155
+ .unwrap_or_default()
156
+ .into_iter()
157
+ .filter_map(|value| action_token(&value, errors))
158
+ .collect()
159
+ }
160
+
161
+ fn action_token(value: &str, errors: &mut Vec<String>) -> Option<IntentKind> {
162
+ match normalized_value(Some(value))?.as_str() {
163
+ "answer" | "advise" | "plan" | "recommend" => Some(IntentKind::Advisory),
164
+ "implement" | "edit" | "modify" | "create_task" => Some(IntentKind::NewTask),
165
+ "revise" | "correct" => Some(IntentKind::TaskRevision),
166
+ "status" => Some(IntentKind::StatusQuestion),
167
+ "review" => Some(IntentKind::ReviewRequest),
168
+ "repair" => Some(IntentKind::RepairRequest),
169
+ "commit" | "baseline" => Some(IntentKind::CommitRequest),
170
+ "no_commit" => Some(IntentKind::NoCommitRequest),
171
+ "cancel" => Some(IntentKind::CancelRequest),
172
+ "complete" => Some(IntentKind::TaskCompletion),
173
+ _ => invalid_value("requestedActions", errors),
174
+ }
175
+ }
176
+
177
+ fn workflow_action(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
58
178
  match normalized_value(value)?.as_str() {
59
179
  "none" | "unknown" => None,
60
180
  "commit_request" => Some(IntentKind::CommitRequest),
@@ -64,20 +184,24 @@ fn workflow_action(value: Option<&str>) -> Option<IntentKind> {
64
184
  "task_completion" => Some(IntentKind::TaskCompletion),
65
185
  "status_question" => Some(IntentKind::StatusQuestion),
66
186
  "no_commit_request" => Some(IntentKind::NoCommitRequest),
67
- _ => None,
187
+ _ => invalid_value("workflowAction", errors),
68
188
  }
69
189
  }
70
190
 
71
- fn task_intent(value: Option<&str>) -> Option<IntentKind> {
191
+ fn task_intent(value: Option<&str>, errors: &mut Vec<String>) -> Option<IntentKind> {
72
192
  match normalized_value(value)?.as_str() {
73
193
  "none" | "unknown" => None,
74
194
  "new_task" => Some(IntentKind::NewTask),
75
195
  "task_revision" => Some(IntentKind::TaskRevision),
76
- _ => None,
196
+ _ => invalid_value("taskIntent", errors),
77
197
  }
78
198
  }
79
199
 
80
- fn envelope_risk_codes(risk: Option<&str>, risk_codes: Option<Vec<String>>) -> Vec<String> {
200
+ fn envelope_risk_codes(
201
+ risk: Option<&str>,
202
+ risk_codes: Option<Vec<String>>,
203
+ errors: &mut Vec<String>,
204
+ ) -> Vec<String> {
81
205
  let mut codes = risk_codes.unwrap_or_default();
82
206
  match normalized_value(risk).as_deref() {
83
207
  Some("none") | Some("unknown") | None => {}
@@ -86,19 +210,49 @@ fn envelope_risk_codes(risk: Option<&str>, risk_codes: Option<Vec<String>>) -> V
86
210
  codes.push("envelope_risk:credential_context".to_string());
87
211
  }
88
212
  Some("bypass_context") => codes.push("envelope_risk:bypass_context".to_string()),
89
- Some(other) => codes.push(format!("envelope_risk:{other}")),
213
+ Some(_) => {
214
+ errors.push("prompt_envelope_invalid:risk".to_string());
215
+ }
90
216
  }
91
217
  codes.sort();
92
218
  codes.dedup();
93
219
  codes
94
220
  }
95
221
 
222
+ fn invalid_value(field: &str, errors: &mut Vec<String>) -> Option<IntentKind> {
223
+ errors.push(format!("prompt_envelope_invalid:{field}"));
224
+ None
225
+ }
226
+
227
+ fn unsupported_value(field: &str, errors: &mut Vec<String>) -> Option<IntentKind> {
228
+ errors.push(format!("prompt_envelope_unsupported:{field}"));
229
+ None
230
+ }
231
+
96
232
  fn normalized_value(value: Option<&str>) -> Option<String> {
97
233
  value
98
234
  .map(|value| value.trim().to_ascii_lowercase())
99
235
  .filter(|value| !value.is_empty())
100
236
  }
101
237
 
238
+ fn raw_string_array(raw: &Value, field: &str) -> Option<Vec<String>> {
239
+ raw.get(field).and_then(Value::as_array).map(|values| {
240
+ values
241
+ .iter()
242
+ .filter_map(Value::as_str)
243
+ .map(ToString::to_string)
244
+ .collect()
245
+ })
246
+ }
247
+
248
+ fn invalid_envelope(validation_errors: Vec<String>, risk: RiskContext) -> RoutingEnvelope {
249
+ RoutingEnvelope {
250
+ candidates: Vec::new(),
251
+ risk,
252
+ validation_errors,
253
+ }
254
+ }
255
+
102
256
  fn candidate(kind: IntentKind, evidence: &str) -> IntentCandidate {
103
257
  IntentCandidate {
104
258
  kind,
@@ -19,6 +19,8 @@ pub(crate) fn response_policy(
19
19
  "auto_commit_harness_refresh_then_completed_task_then_create_new_task" => "The completed task is valid after separating deterministic harness refresh changes. NAOME can baseline the refresh first, then baseline the task before starting the next separate task.",
20
20
  "auto_commit_upgrade_baseline_then_create_new_task" => "The setup or repair diff is ready. NAOME can baseline it automatically before starting the next task.",
21
21
  "create_new_task" | "create_new_task_without_auto_baseline" => "NAOME is ready to create the next task.",
22
+ "normalize_prompt_first" => "NAOME needs a structured prompt envelope before it can route or mutate repository state.",
23
+ "answer_without_mutation" => "The prompt is advisory or planning-only, so NAOME will not mutate repository state.",
22
24
  "answer_status_only" => summary,
23
25
  "block_auto_baseline_due_to_no_commit" => "I will not baseline or commit because the prompt explicitly blocks committing. Review, revise, cancel, or clear the current diff first.",
24
26
  "review_task_diff" | "review_diff_first" | "review_current_task_diff" => "The prompt asks for review, so NAOME will not mutate the repository automatically.",
@@ -18,6 +18,8 @@ pub(crate) struct PromptSegment {
18
18
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19
19
  pub(crate) enum IntentKind {
20
20
  Ambiguous,
21
+ PromptNormalizationRequired,
22
+ Advisory,
21
23
  NewTask,
22
24
  TaskRevision,
23
25
  StatusQuestion,
@@ -34,6 +36,8 @@ impl IntentKind {
34
36
  pub fn as_str(self) -> &'static str {
35
37
  match self {
36
38
  Self::Ambiguous => "ambiguous",
39
+ Self::PromptNormalizationRequired => "prompt_normalization_required",
40
+ Self::Advisory => "advisory",
37
41
  Self::NewTask => "new_task",
38
42
  Self::TaskRevision => "task_revision",
39
43
  Self::StatusQuestion => "status_question",
@@ -64,8 +68,10 @@ pub(crate) struct RiskContext {
64
68
  #[derive(Debug, Clone, PartialEq, Eq)]
65
69
  pub(crate) struct CanonicalIntent {
66
70
  pub is_empty: bool,
71
+ pub has_routing_envelope: bool,
67
72
  pub references_current_task: bool,
68
73
  pub candidates: Vec<IntentCandidate>,
69
74
  pub risk: RiskContext,
70
75
  pub has_workflow_conflict: bool,
76
+ pub validation_errors: Vec<String>,
71
77
  }
@@ -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