@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
@@ -1,6 +1,259 @@
1
1
  mod intent_support;
2
2
 
3
- use intent_support::{env, idle, Repo};
3
+ use intent_support::{idle, legacy_env, prompt_env, prompt_env_with_actions, Repo};
4
+
5
+ #[test]
6
+ fn raw_prompts_require_structured_prompt_envelope_before_routing() {
7
+ let repo = Repo::clean("intent-v2-raw-prompt-envelope-required", idle());
8
+ for prompt in [
9
+ "What would you recommend for the next minor release?",
10
+ "Bitte sauber umsetzen, committen, pushen und MR erstellen.",
11
+ "Please implement a commit gate simulator.",
12
+ ] {
13
+ let intent = repo.intent(prompt);
14
+ assert_eq!(
15
+ intent.prompt_intent, "prompt_normalization_required",
16
+ "{prompt}"
17
+ );
18
+ assert_eq!(intent.policy_action, "normalize_prompt_first", "{prompt}");
19
+ assert!(intent.allowed, "{prompt}");
20
+ assert!(!intent.evidence.requests_new_goal, "{prompt}");
21
+ assert!(!intent.evidence.requests_commit, "{prompt}");
22
+ assert!(!intent.evidence.requests_review, "{prompt}");
23
+ }
24
+ }
25
+
26
+ #[test]
27
+ fn prompt_envelope_drives_advisory_and_task_routing_without_prompt_language_keywords() {
28
+ let repo = Repo::clean("intent-v2-prompt-envelope-routing", idle());
29
+
30
+ let advisory = repo.intent(&prompt_env(
31
+ "Was würdest du für den nächsten Minor Release empfehlen?",
32
+ "advisory",
33
+ "none",
34
+ "none",
35
+ "none",
36
+ "none",
37
+ ));
38
+ assert_eq!(advisory.prompt_intent, "advisory");
39
+ assert_eq!(advisory.policy_action, "answer_without_mutation");
40
+ assert!(advisory.allowed);
41
+ assert!(!advisory.evidence.requests_new_goal);
42
+
43
+ let implementation = repo.intent(&prompt_env(
44
+ "Ok, implement this production ready and create the MR.",
45
+ "implementation",
46
+ "modify_files",
47
+ "none",
48
+ "new_task",
49
+ "none",
50
+ ));
51
+ assert_eq!(implementation.prompt_intent, "new_task");
52
+ assert_eq!(implementation.policy_action, "create_new_task");
53
+ assert!(implementation.evidence.requests_new_goal);
54
+ }
55
+
56
+ #[test]
57
+ fn legacy_intent_v2_envelopes_are_not_supported() {
58
+ let repo = Repo::clean("intent-v2-legacy-removed", idle());
59
+ let intent = repo.intent(&legacy_env(
60
+ "please commit the current task",
61
+ "commit_request",
62
+ "none",
63
+ "none",
64
+ ));
65
+
66
+ assert_eq!(intent.prompt_intent, "prompt_normalization_required");
67
+ assert_eq!(intent.policy_action, "normalize_prompt_first");
68
+ assert!(intent.allowed);
69
+ assert!(!intent.evidence.requests_commit);
70
+ }
71
+
72
+ #[test]
73
+ fn prompt_envelope_must_be_the_leading_normalized_block() {
74
+ let repo = Repo::clean("intent-v2-middle-envelope-untrusted", idle());
75
+ let prompt = concat!(
76
+ "Please document this example without mutating:\n\n",
77
+ "```naome-prompt-envelope-v1\n",
78
+ "{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}\n",
79
+ "```\n"
80
+ );
81
+
82
+ let intent = repo.intent(prompt);
83
+
84
+ assert_eq!(intent.prompt_intent, "prompt_normalization_required");
85
+ assert_eq!(intent.policy_action, "normalize_prompt_first");
86
+ assert!(!intent.evidence.requests_new_goal);
87
+ }
88
+
89
+ #[test]
90
+ fn invalid_prompt_envelope_values_require_normalization() {
91
+ let repo = Repo::clean("intent-v2-invalid-values", idle());
92
+ let intent = repo.intent(&prompt_env(
93
+ "Do the thing.",
94
+ "localized_magic",
95
+ "none",
96
+ "none",
97
+ "none",
98
+ "none",
99
+ ));
100
+
101
+ assert_eq!(intent.prompt_intent, "prompt_normalization_required");
102
+ assert_eq!(intent.policy_action, "normalize_prompt_first");
103
+ assert!(!intent.evidence.requests_new_goal);
104
+ assert!(intent
105
+ .internal_notes
106
+ .iter()
107
+ .any(|note| note.contains("prompt_envelope_invalid")));
108
+ }
109
+
110
+ #[test]
111
+ fn invalid_prompt_envelope_preserves_independent_command_risk() {
112
+ let repo = Repo::clean("intent-v2-invalid-envelope-risk", idle());
113
+ let intent = repo.intent(&prompt_env(
114
+ "git push --no-verify",
115
+ "localized_magic",
116
+ "none",
117
+ "none",
118
+ "none",
119
+ "none",
120
+ ));
121
+
122
+ assert_eq!(intent.prompt_intent, "unsafe");
123
+ assert_eq!(intent.policy_action, "block_unsafe_intent");
124
+ assert!(intent.evidence.has_risky_terms);
125
+ assert!(intent
126
+ .risk_codes
127
+ .contains(&"structured_risk:bypass_command".to_string()));
128
+ }
129
+
130
+ #[test]
131
+ fn invalid_prompt_envelope_preserves_declared_envelope_risk() {
132
+ let repo = Repo::clean("intent-v2-invalid-envelope-declared-risk", idle());
133
+ let prompt = concat!(
134
+ "```naome-prompt-envelope-v1\n",
135
+ "{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"publicationIntent\":\"push\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"credential_context\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}\n",
136
+ "```"
137
+ );
138
+
139
+ let intent = repo.intent(prompt);
140
+
141
+ assert_eq!(intent.prompt_intent, "unsafe");
142
+ assert_eq!(intent.policy_action, "block_unsafe_intent");
143
+ assert!(intent.evidence.has_risky_terms);
144
+ assert!(intent
145
+ .risk_codes
146
+ .contains(&"envelope_risk:credential_context".to_string()));
147
+ }
148
+
149
+ #[test]
150
+ fn leading_prompt_envelope_shape_errors_require_normalization() {
151
+ let repo = Repo::clean("intent-v2-invalid-envelope-shape", idle());
152
+ let prompt = concat!(
153
+ "```naome-prompt-envelope-v1\n",
154
+ "{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":\"commit\",\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}\n",
155
+ "```"
156
+ );
157
+
158
+ let intent = repo.intent(prompt);
159
+
160
+ assert_eq!(intent.prompt_intent, "prompt_normalization_required");
161
+ assert_eq!(intent.policy_action, "normalize_prompt_first");
162
+ assert!(intent
163
+ .internal_notes
164
+ .iter()
165
+ .any(|note| note.contains("prompt_envelope_invalid")));
166
+ }
167
+
168
+ #[test]
169
+ fn publication_requests_require_normalization_instead_of_becoming_new_tasks() {
170
+ let repo = Repo::clean("intent-v2-publication-unsupported", idle());
171
+ let prompt = concat!(
172
+ "```naome-prompt-envelope-v1\n",
173
+ "{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"commit\",\"publicationIntent\":\"push\",\"workflowAction\":\"none\",\"taskIntent\":\"none\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}\n",
174
+ "```\n\nCommit and push this task."
175
+ );
176
+
177
+ let intent = repo.intent(prompt);
178
+
179
+ assert_eq!(intent.prompt_intent, "prompt_normalization_required");
180
+ assert_eq!(intent.policy_action, "normalize_prompt_first");
181
+ assert!(!intent.evidence.requests_commit);
182
+ assert!(intent
183
+ .internal_notes
184
+ .iter()
185
+ .any(|note| note.contains("prompt_envelope_unsupported:publicationIntent")));
186
+ }
187
+
188
+ #[test]
189
+ fn publication_mutation_intents_require_normalization() {
190
+ let repo = Repo::clean("intent-v2-publication-mutation-unsupported", idle());
191
+ let prompt = concat!(
192
+ "```naome-prompt-envelope-v1\n",
193
+ "{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"push\",\"publicationIntent\":\"none\",\"workflowAction\":\"none\",\"taskIntent\":\"none\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}\n",
194
+ "```"
195
+ );
196
+
197
+ let intent = repo.intent(prompt);
198
+
199
+ assert_eq!(intent.prompt_intent, "prompt_normalization_required");
200
+ assert_eq!(intent.policy_action, "normalize_prompt_first");
201
+ assert!(!intent.evidence.requests_new_goal);
202
+ assert!(intent
203
+ .internal_notes
204
+ .iter()
205
+ .any(|note| note.contains("prompt_envelope_unsupported:mutationIntent")));
206
+ }
207
+
208
+ #[test]
209
+ fn envelope_only_prompts_route_from_the_envelope() {
210
+ let repo = Repo::clean("intent-v2-envelope-only", idle());
211
+ let prompt = concat!(
212
+ "```naome-prompt-envelope-v1\n",
213
+ "{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}\n",
214
+ "```"
215
+ );
216
+
217
+ let intent = repo.intent(prompt);
218
+
219
+ assert_eq!(intent.prompt_intent, "new_task");
220
+ assert_eq!(intent.policy_action, "create_new_task");
221
+ }
222
+
223
+ #[test]
224
+ fn compatible_status_answer_envelopes_are_not_ambiguous() {
225
+ let repo = Repo::clean("intent-v2-status-answer", idle());
226
+ let intent = repo.intent(&prompt_env_with_actions(
227
+ "",
228
+ "status",
229
+ "none",
230
+ "none",
231
+ "none",
232
+ "none",
233
+ &["answer"],
234
+ ));
235
+
236
+ assert_eq!(intent.prompt_intent, "status_question");
237
+ assert_eq!(intent.policy_action, "answer_status_only");
238
+ }
239
+
240
+ #[test]
241
+ fn conflicting_prompt_envelope_values_block_as_ambiguous() {
242
+ let repo = Repo::clean("intent-v2-conflicting-values", idle());
243
+ let intent = repo.intent(&prompt_env_with_actions(
244
+ "Only advise, but also edit files.",
245
+ "advisory",
246
+ "modify_files",
247
+ "none",
248
+ "none",
249
+ "none",
250
+ &[],
251
+ ));
252
+
253
+ assert_eq!(intent.prompt_intent, "ambiguous");
254
+ assert_eq!(intent.policy_action, "block_ambiguous_intent");
255
+ assert!(!intent.allowed);
256
+ }
4
257
 
5
258
  #[test]
6
259
  fn feature_descriptions_with_workflow_terms_route_as_new_tasks() {
@@ -11,7 +264,14 @@ fn feature_descriptions_with_workflow_terms_route_as_new_tasks() {
11
264
  "Implement a review findings feature for pull request analysis.",
12
265
  "Create a baseline flow visualization in the UI.",
13
266
  ] {
14
- let intent = repo.intent(prompt);
267
+ let intent = repo.intent(&prompt_env(
268
+ prompt,
269
+ "implementation",
270
+ "modify_files",
271
+ "none",
272
+ "new_task",
273
+ "none",
274
+ ));
15
275
  assert_eq!(intent.prompt_intent, "new_task", "{prompt}");
16
276
  assert_eq!(intent.policy_action, "create_new_task", "{prompt}");
17
277
  assert!(!intent.evidence.requests_commit, "{prompt}");
@@ -23,8 +283,10 @@ fn feature_descriptions_with_workflow_terms_route_as_new_tasks() {
23
283
  #[test]
24
284
  fn enveloped_workflow_requests_keep_their_intent() {
25
285
  let repo = Repo::clean("intent-v2-direct-workflow", idle());
26
- let commit = repo.intent(&env(
286
+ let commit = repo.intent(&prompt_env(
27
287
  "please commit the current task",
288
+ "implementation",
289
+ "commit",
28
290
  "commit_request",
29
291
  "none",
30
292
  "none",
@@ -32,16 +294,26 @@ fn enveloped_workflow_requests_keep_their_intent() {
32
294
  assert_eq!(commit.prompt_intent, "commit_request");
33
295
  assert!(commit.evidence.requests_commit);
34
296
 
35
- let review = repo.intent(&env(
297
+ let review = repo.intent(&prompt_env_with_actions(
36
298
  "review the current diff",
299
+ "implementation",
300
+ "none",
37
301
  "review_request",
38
302
  "none",
39
303
  "none",
304
+ &["review"],
40
305
  ));
41
306
  assert_eq!(review.prompt_intent, "review_request");
42
307
  assert!(review.evidence.requests_review);
43
308
 
44
- let status = repo.intent(&env("show status", "status_question", "none", "none"));
309
+ let status = repo.intent(&prompt_env(
310
+ "show status",
311
+ "status",
312
+ "none",
313
+ "status_question",
314
+ "none",
315
+ "none",
316
+ ));
45
317
  assert_eq!(status.prompt_intent, "status_question");
46
318
  assert_eq!(status.policy_action, "answer_status_only");
47
319
  }
@@ -49,8 +321,10 @@ fn enveloped_workflow_requests_keep_their_intent() {
49
321
  #[test]
50
322
  fn envelope_is_canonical_over_legacy_prompt_words() {
51
323
  let repo = Repo::clean("intent-v2-envelope-canonical", idle());
52
- let intent = repo.intent(&env(
324
+ let intent = repo.intent(&prompt_env(
53
325
  "please commit the current task",
326
+ "implementation",
327
+ "modify_files",
54
328
  "none",
55
329
  "new_task",
56
330
  "none",
@@ -62,12 +336,25 @@ fn envelope_is_canonical_over_legacy_prompt_words() {
62
336
  #[test]
63
337
  fn token_boundaries_and_code_segments_avoid_false_workflow_actions() {
64
338
  let repo = Repo::clean("intent-v2-segments", idle());
65
- let async_intent = repo.intent("Add async sync-state naming examples to the documentation.");
339
+ let async_intent = repo.intent(&prompt_env(
340
+ "Add async sync-state naming examples to the documentation.",
341
+ "implementation",
342
+ "modify_files",
343
+ "none",
344
+ "new_task",
345
+ "none",
346
+ ));
66
347
  assert_eq!(async_intent.prompt_intent, "new_task");
67
348
  assert!(!async_intent.evidence.requests_repair);
68
349
 
69
- let code_intent =
70
- repo.intent("Add docs with examples like `naome commit` and ```sh\ngit push\n```.");
350
+ let code_intent = repo.intent(&prompt_env(
351
+ "Add docs with examples like `naome commit` and ```sh\ngit push\n```.",
352
+ "implementation",
353
+ "modify_files",
354
+ "none",
355
+ "new_task",
356
+ "none",
357
+ ));
71
358
  assert_eq!(code_intent.prompt_intent, "new_task");
72
359
  assert!(!code_intent.evidence.requests_commit);
73
360
  assert!(!code_intent.evidence.has_risky_terms);
@@ -76,8 +363,10 @@ fn token_boundaries_and_code_segments_avoid_false_workflow_actions() {
76
363
  #[test]
77
364
  fn explicit_credential_context_still_routes_as_unsafe() {
78
365
  let repo = Repo::clean("intent-v2-risk", idle());
79
- let intent = repo.intent(&env(
366
+ let intent = repo.intent(&prompt_env(
80
367
  "Commit the current token and API key so the deployment can use the secret.",
368
+ "implementation",
369
+ "commit",
81
370
  "commit_request",
82
371
  "none",
83
372
  "credential_context",
@@ -6,10 +6,16 @@ use super::TestRepo;
6
6
 
7
7
  const NEW_README_TASK_PROMPT: &str = "Add another line to README as a new task.";
8
8
 
9
+ pub fn prompt_env(prompt: &str, workflow: &str, task: &str, mutation_intent: &str) -> String {
10
+ format!(
11
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"{mutation_intent}\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
12
+ )
13
+ }
14
+
9
15
  pub fn route_commit_request(repo: &TestRepo) -> RouteDecision {
10
16
  evaluate_route(
11
17
  repo.path(),
12
- "commit my changes",
18
+ &prompt_env("commit my changes", "commit_request", "none", "commit"),
13
19
  RouteOptions {
14
20
  execute: true,
15
21
  evaluation: EvaluationOptions::offline(),
@@ -29,7 +35,7 @@ pub fn try_route_new_task(
29
35
  ) -> Result<RouteDecision, NaomeError> {
30
36
  evaluate_route(
31
37
  repo.path(),
32
- prompt,
38
+ &prompt_env(prompt, "none", "new_task", "modify_files"),
33
39
  RouteOptions {
34
40
  execute,
35
41
  evaluation: EvaluationOptions::offline(),
@@ -3,11 +3,40 @@ use std::fs;
3
3
 
4
4
  mod repo_support;
5
5
 
6
+ use naome_core::{evaluate_route, EvaluationOptions, RouteOptions};
6
7
  use repo_support::{
7
8
  assert_commit_paths, assert_isolated_worktree_ready, route_new_task, route_readme_task,
8
9
  TestRepo,
9
10
  };
10
11
 
12
+ #[test]
13
+ fn execute_route_with_raw_prompt_requests_normalization_without_mutating() {
14
+ let repo = TestRepo::new("route-raw-prompt-normalize-first");
15
+ repo.init_git();
16
+ repo.write_file("README.md", "# Baseline\n");
17
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
18
+ repo.git(&["add", "."]);
19
+ repo.git(&["commit", "-m", "baseline"]);
20
+ let before = repo.git_stdout(&["rev-parse", "HEAD"]);
21
+
22
+ let route = evaluate_route(
23
+ repo.path(),
24
+ "Please implement, commit, push, and create the MR.",
25
+ RouteOptions {
26
+ execute: true,
27
+ evaluation: EvaluationOptions::offline(),
28
+ },
29
+ )
30
+ .unwrap();
31
+
32
+ assert_eq!(route.prompt_intent, "prompt_normalization_required");
33
+ assert_eq!(route.policy_action, "normalize_prompt_first");
34
+ assert!(!route.mutation_performed);
35
+ assert!(!route.can_create_task);
36
+ assert_eq!(repo.git_stdout(&["rev-parse", "HEAD"]), before);
37
+ assert!(repo.git_status_short().is_empty());
38
+ }
39
+
11
40
  #[test]
12
41
  fn dry_route_reports_auto_baseline_without_mutating() {
13
42
  let repo = TestRepo::completed_task_with_diff("route-dry-auto");
@@ -7,6 +7,12 @@ mod repo_support;
7
7
 
8
8
  use repo_support::TestRepo;
9
9
 
10
+ fn route_prompt(prompt: &str, workflow: &str, task: &str, mutation_intent: &str) -> String {
11
+ format!(
12
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"{mutation_intent}\",\"workflowAction\":\"{workflow}\",\"taskIntent\":\"{task}\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
13
+ )
14
+ }
15
+
10
16
  #[test]
11
17
  fn execute_route_does_not_mutate_when_prompt_blocks_commit() {
12
18
  let repo = TestRepo::completed_task_with_diff("route-no-commit");
@@ -14,7 +20,12 @@ fn execute_route_does_not_mutate_when_prompt_blocks_commit() {
14
20
 
15
21
  let route = evaluate_route(
16
22
  repo.path(),
17
- "Do not commit. Start a new task after this.",
23
+ &route_prompt(
24
+ "Do not commit. Start a new task after this.",
25
+ "no_commit_request",
26
+ "new_task",
27
+ "none",
28
+ ),
18
29
  RouteOptions {
19
30
  execute: true,
20
31
  evaluation: EvaluationOptions::offline(),
@@ -41,7 +52,7 @@ fn explicit_route_commit_baseline_leaves_unrelated_user_edit_unstaged() {
41
52
 
42
53
  let route = evaluate_route(
43
54
  repo.path(),
44
- "commit_task_baseline",
55
+ &route_prompt("commit_task_baseline", "commit_request", "none", "commit"),
45
56
  RouteOptions {
46
57
  execute: true,
47
58
  evaluation: EvaluationOptions::offline(),
@@ -67,7 +78,12 @@ fn execute_route_journals_external_commit_after_completed_task() {
67
78
 
68
79
  let route = evaluate_route(
69
80
  repo.path(),
70
- "Create a new task for README polish.",
81
+ &route_prompt(
82
+ "Create a new task for README polish.",
83
+ "none",
84
+ "new_task",
85
+ "modify_files",
86
+ ),
71
87
  RouteOptions {
72
88
  execute: true,
73
89
  evaluation: EvaluationOptions::offline(),
@@ -94,7 +110,12 @@ fn explain_reports_winning_rule_and_mutation_plan_without_executing() {
94
110
 
95
111
  let explain = explain_route(
96
112
  repo.path(),
97
- "Start a new task for README polish.",
113
+ &route_prompt(
114
+ "Start a new task for README polish.",
115
+ "none",
116
+ "new_task",
117
+ "modify_files",
118
+ ),
98
119
  EvaluationOptions::offline(),
99
120
  )
100
121
  .unwrap();
@@ -126,7 +147,7 @@ fn unhealthy_harness_route_blocks_normal_work() {
126
147
 
127
148
  let route = evaluate_route(
128
149
  repo.path(),
129
- "Create a new task.",
150
+ &route_prompt("Create a new task.", "none", "new_task", "modify_files"),
130
151
  RouteOptions {
131
152
  execute: true,
132
153
  evaluation: EvaluationOptions::online(),
@@ -9,6 +9,12 @@ use repo_support::{
9
9
  TestRepo,
10
10
  };
11
11
 
12
+ fn repair_prompt(prompt: &str) -> String {
13
+ format!(
14
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"fix\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"repair_request\",\"taskIntent\":\"none\",\"risk\":\"none\",\"requestedActions\":[\"repair\"],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
15
+ )
16
+ }
17
+
12
18
  #[test]
13
19
  fn execute_route_baselines_harness_refresh_before_dirty_repo_worktree() {
14
20
  let repo = TestRepo::dirty_harness_refresh_repo("route-dirty-harness-refresh-worktree", true);
@@ -66,7 +72,7 @@ fn execute_route_repair_request_baselines_harness_refresh_only() {
66
72
 
67
73
  let route = evaluate_route(
68
74
  repo.path(),
69
- "please repair all",
75
+ &repair_prompt("please repair all"),
70
76
  RouteOptions {
71
77
  execute: true,
72
78
  evaluation: EvaluationOptions::offline(),
@@ -27,7 +27,7 @@ fn execute_route_does_not_mutate_or_offer_clear_commit_for_dirty_unowned_diff()
27
27
  )
28
28
  .unwrap();
29
29
 
30
- assert_eq!(route.policy_action, "block_unowned_diff");
30
+ assert_eq!(route.policy_action, "normalize_prompt_first");
31
31
  assert!(!route.mutation_performed);
32
32
  assert!(!route.can_create_task);
33
33
  assert_eq!(route.executed_actions, Vec::<String>::new());
@@ -6,6 +6,12 @@ use task_state_compact_support::{
6
6
  compact_state, large_compact_state, large_expanded_state, legacy_state, MiniRepo,
7
7
  };
8
8
 
9
+ fn new_task_prompt(prompt: &str) -> String {
10
+ format!(
11
+ "```naome-prompt-envelope-v1\n{{\"schema\":\"naome.prompt-envelope.v1\",\"requestKind\":\"implementation\",\"mutationIntent\":\"modify_files\",\"workflowAction\":\"none\",\"taskIntent\":\"new_task\",\"risk\":\"none\",\"requestedActions\":[],\"referencedPaths\":[],\"constraints\":[],\"uncertainties\":[]}}\n```\n\n{prompt}"
12
+ )
13
+ }
14
+
9
15
  #[test]
10
16
  fn legacy_v1_proof_results_remain_valid() {
11
17
  let repo = MiniRepo::new();
@@ -30,7 +36,7 @@ fn compact_path_sets_and_batches_cover_completion_commit_and_route() {
30
36
 
31
37
  let route = evaluate_route(
32
38
  repo.path(),
33
- "new task",
39
+ &new_task_prompt("new task"),
34
40
  RouteOptions {
35
41
  execute: false,
36
42
  evaluation: EvaluationOptions::offline(),
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamentis/naome",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "Native-first CLI for the NAOME agent harness.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  {
2
- "harnessVersion": "1.2.0",
2
+ "harnessVersion": "1.3.8",
3
3
  "installedAt": null,
4
4
  "integrity": {
5
5
  ".naome/bin/check-harness-health.js": "sha256:dc4de52b79c69600b9ba47b924e2c2b8de61a2cbfab6d1ccc0f1924d963db657",
@@ -8,7 +8,7 @@
8
8
  ".naome/package.json": "sha256:8005a3491db7d92f36ac66369861589f9c47123d3a7c71e643fc2c06168cd45a",
9
9
  ".naome/task-contract.schema.json": "sha256:1b3b62350328d0d6d660e36d1d1baaa2b88718530db774f9ab2a9e2fcba369c8",
10
10
  "AGENTS.md": "sha256:e8b2fc786c1c72b69ba8f2b2ffce4f459e799c7453ce9ff4a9f6448a8f9e6b4f",
11
- "docs/naome/agent-workflow.md": "sha256:cbef6dee1543b4c74111f8dc23e81a5bf092a9585a342504678bfc9c9d0655ea",
11
+ "docs/naome/agent-workflow.md": "sha256:0be1c29adfbcd3fd73c4f904080ffc67237692fe413871a30243538c4db38ac7",
12
12
  "docs/naome/context-economy.md": "sha256:3ed5075815ecf4ada46a5e65438769310307c35759fcd46b13dc0b96e02bebd9",
13
13
  "docs/naome/execution.md": "sha256:bfc5d55838942ec8e3d790b59e3c634ff5bf6a2298265cef3dca9788a097eafb",
14
14
  "docs/naome/first-run.md": "sha256:1466ce8c65e19a1514885f917db14e8a772350e3f6d1c03a66326963365919e1",
@@ -11,11 +11,20 @@ Use this workflow after first-run intake is complete.
11
11
  `canCreateTask` fields instead of inventing routing or final-response text.
12
12
  If route returns a `taskRoot` different from the current directory, continue
13
13
  the task from that path and leave the original worktree untouched.
14
- When preparing the route prompt file, prepend a fenced `naome-intent-v2`
15
- JSON envelope that states `workflowAction`, `taskIntent`, and `risk` using
16
- canonical NAOME schema constants such as `commit_request`,
17
- `task_revision`, and `credential_context`. Keep the original user text below
18
- it. Do not derive workflow actions from natural-language keywords.
14
+ When preparing the route prompt file, first normalize the user's raw prompt
15
+ into a fenced `naome-prompt-envelope-v1` JSON envelope. The envelope is the
16
+ routing input; the raw user text below it is audit text. Use canonical NAOME
17
+ schema fields such as `requestKind`, `mutationIntent`,
18
+ `publicationIntent`, `requestedActions`, `referencedPaths`, `constraints`,
19
+ `uncertainties`, `workflowAction`, `taskIntent`, and `risk`. Do not derive
20
+ workflow actions from natural-language keywords, localized words, or the
21
+ prompt's prose language. If a prompt file has no envelope, route returns a
22
+ non-mutating `normalize_prompt_first` decision and the agent must add the
23
+ deterministic envelope before retrying. `naome-intent-v2` is not supported.
24
+ Unknown envelope values require normalization, and conflicting envelope
25
+ fields block as ambiguous instead of mutating. Put concrete files in
26
+ `referencedPaths`; prompt context selection treats those paths as the
27
+ primary source of path intent.
19
28
  3. Use `node .naome/bin/naome.js explain --prompt-file <path> --json` only when
20
29
  debugging why a policy won.
21
30
  4. Run `node .naome/bin/naome.js status --json` when reporting state without
@@ -18,6 +18,15 @@ Status: Uninitialized
18
18
  - Per-check proof files are local run evidence. Prefer compact proof batches in
19
19
  the committed task-state projection when many release checks share the same
20
20
  evidence paths.
21
+ - Prompt routing uses fenced `naome-prompt-envelope-v1` JSON envelopes as the
22
+ deterministic routing input. Raw natural-language prompts are audit text and
23
+ must not be treated as workflow authority until an agent normalizes them into
24
+ canonical fields such as `requestKind`, `mutationIntent`,
25
+ `publicationIntent`, `requestedActions`, `workflowAction`, `taskIntent`, and
26
+ `risk`. Legacy `naome-intent-v2` envelopes are not supported. Unknown
27
+ envelope values require prompt normalization, conflicting fields block as
28
+ ambiguous, and `referencedPaths` is the primary context-selection source for
29
+ prompt-mentioned files.
21
30
 
22
31
  ## Assumed Boundaries
23
32