@lamentis/naome 1.3.6 → 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.
- package/Cargo.lock +2 -2
- package/README.md +9 -3
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/context/select.rs +58 -4
- package/crates/naome-core/src/git.rs +1 -26
- package/crates/naome-core/src/information_architecture.rs +15 -26
- package/crates/naome-core/src/install_plan.rs +2 -0
- package/crates/naome-core/src/intent/classifier.rs +56 -81
- package/crates/naome-core/src/intent/envelope.rs +173 -19
- package/crates/naome-core/src/intent/legacy_response.rs +2 -0
- package/crates/naome-core/src/intent/model.rs +6 -0
- package/crates/naome-core/src/intent/resolver.rs +25 -0
- package/crates/naome-core/src/intent/risk.rs +11 -1
- package/crates/naome-core/src/intent.rs +1 -1
- package/crates/naome-core/src/lib.rs +1 -0
- package/crates/naome-core/src/paths.rs +27 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +2 -47
- package/crates/naome-core/src/repo_paths.rs +76 -0
- package/crates/naome-core/src/repository_model/path_scan.rs +2 -23
- package/crates/naome-core/src/route/context.rs +8 -0
- package/crates/naome-core/src/task_ledger/write.rs +6 -0
- package/crates/naome-core/src/task_ledger.rs +50 -2
- package/crates/naome-core/src/task_state/util.rs +21 -17
- package/crates/naome-core/tests/context.rs +92 -0
- package/crates/naome-core/tests/decision.rs +2 -10
- package/crates/naome-core/tests/information_architecture.rs +7 -10
- package/crates/naome-core/tests/install_plan.rs +2 -0
- package/crates/naome-core/tests/intent.rs +98 -18
- package/crates/naome-core/tests/intent_support/mod.rs +39 -1
- package/crates/naome-core/tests/intent_v2.rs +299 -10
- package/crates/naome-core/tests/quality_structure_support/mod.rs +6 -25
- package/crates/naome-core/tests/repo_support/mod.rs +3 -5
- package/crates/naome-core/tests/repo_support/repo_helpers.rs +2 -9
- package/crates/naome-core/tests/repo_support/routes.rs +8 -2
- package/crates/naome-core/tests/repo_support/verification.rs +1 -8
- package/crates/naome-core/tests/repo_support/verification_values.rs +20 -18
- package/crates/naome-core/tests/route_baseline.rs +29 -0
- package/crates/naome-core/tests/route_completion.rs +26 -5
- package/crates/naome-core/tests/route_harness_refresh.rs +7 -1
- package/crates/naome-core/tests/route_user_diff.rs +1 -1
- package/crates/naome-core/tests/task_ledger.rs +69 -1
- package/crates/naome-core/tests/task_state_compact.rs +7 -1
- package/crates/naome-core/tests/task_state_compact_support/states.rs +10 -8
- package/crates/naome-core/tests/task_state_support/states.rs +8 -8
- package/crates/naome-core/tests/workflow_support/mod.rs +2 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/manifest.json +3 -3
- package/templates/naome-root/.naomeignore +1 -0
- package/templates/naome-root/docs/naome/agent-workflow.md +22 -15
- package/templates/naome-root/docs/naome/architecture.md +17 -8
- package/templates/naome-root/docs/naome/task-ledger.md +20 -17
- package/crates/naome-core/src/intent/patterns.rs +0 -170
|
@@ -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
|
|
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 =
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
98
|
+
Some(RoutingEnvelope {
|
|
99
|
+
candidates,
|
|
100
|
+
risk,
|
|
101
|
+
validation_errors,
|
|
102
|
+
})
|
|
45
103
|
}
|
|
46
104
|
|
|
47
|
-
fn
|
|
48
|
-
let
|
|
49
|
-
let after_open =
|
|
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
|
|
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
|
-
_ =>
|
|
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
|
-
_ =>
|
|
196
|
+
_ => invalid_value("taskIntent", errors),
|
|
77
197
|
}
|
|
78
198
|
}
|
|
79
199
|
|
|
80
|
-
fn envelope_risk_codes(
|
|
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(
|
|
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
|
|
|
@@ -1,9 +1,36 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
pub fn naomeignore_patterns(root: &Path) -> Vec<String> {
|
|
5
|
+
let Ok(content) = fs::read_to_string(root.join(".naomeignore")) else {
|
|
6
|
+
return Vec::new();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let mut patterns = content
|
|
10
|
+
.lines()
|
|
11
|
+
.map(str::trim)
|
|
12
|
+
.filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
|
|
13
|
+
.map(normalize_ignore_pattern)
|
|
14
|
+
.collect::<Vec<_>>();
|
|
15
|
+
patterns.push(".naome/cache/**".to_string());
|
|
16
|
+
patterns
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
pub fn matches_any(path: &str, patterns: &[String]) -> bool {
|
|
2
20
|
patterns
|
|
3
21
|
.iter()
|
|
4
22
|
.any(|pattern| matches_pattern(path, pattern))
|
|
5
23
|
}
|
|
6
24
|
|
|
25
|
+
fn normalize_ignore_pattern(pattern: &str) -> String {
|
|
26
|
+
let normalized = pattern.trim_start_matches("./").replace('\\', "/");
|
|
27
|
+
if normalized.ends_with('/') {
|
|
28
|
+
format!("{normalized}**")
|
|
29
|
+
} else {
|
|
30
|
+
normalized
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
fn matches_pattern(path: &str, pattern: &str) -> bool {
|
|
8
35
|
let normalized_path = path.replace('\\', "/");
|
|
9
36
|
let normalized_pattern = pattern.replace('\\', "/");
|
|
@@ -4,36 +4,10 @@ use std::path::Path;
|
|
|
4
4
|
use std::process::Command;
|
|
5
5
|
|
|
6
6
|
use crate::models::NaomeError;
|
|
7
|
+
use crate::repo_paths::{collect_tracked_and_untracked_paths, RepoPathFallback};
|
|
7
8
|
|
|
8
9
|
pub(crate) fn collect_repo_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
9
|
-
|
|
10
|
-
.args([
|
|
11
|
-
"ls-files",
|
|
12
|
-
"-z",
|
|
13
|
-
"--cached",
|
|
14
|
-
"--others",
|
|
15
|
-
"--exclude-standard",
|
|
16
|
-
])
|
|
17
|
-
.current_dir(root)
|
|
18
|
-
.output();
|
|
19
|
-
if let Ok(output) = output {
|
|
20
|
-
if output.status.success() {
|
|
21
|
-
let mut paths = output
|
|
22
|
-
.stdout
|
|
23
|
-
.split(|byte| *byte == 0)
|
|
24
|
-
.filter(|entry| !entry.is_empty())
|
|
25
|
-
.map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
|
|
26
|
-
.collect::<Vec<_>>();
|
|
27
|
-
paths.sort();
|
|
28
|
-
paths.dedup();
|
|
29
|
-
return Ok(paths);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let mut paths = Vec::new();
|
|
34
|
-
collect_files_recursive(root, root, &mut paths)?;
|
|
35
|
-
paths.sort();
|
|
36
|
-
Ok(paths)
|
|
10
|
+
collect_tracked_and_untracked_paths(root, RepoPathFallback::Filesystem)
|
|
37
11
|
}
|
|
38
12
|
|
|
39
13
|
pub(super) fn added_lines_by_path(
|
|
@@ -103,25 +77,6 @@ pub(super) fn tracked_blob_hashes(root: &Path) -> Result<HashMap<String, String>
|
|
|
103
77
|
Ok(hashes)
|
|
104
78
|
}
|
|
105
79
|
|
|
106
|
-
fn collect_files_recursive(
|
|
107
|
-
root: &Path,
|
|
108
|
-
dir: &Path,
|
|
109
|
-
paths: &mut Vec<String>,
|
|
110
|
-
) -> Result<(), NaomeError> {
|
|
111
|
-
for entry in fs::read_dir(dir)? {
|
|
112
|
-
let entry = entry?;
|
|
113
|
-
let path = entry.path();
|
|
114
|
-
if path.is_dir() {
|
|
115
|
-
collect_files_recursive(root, &path, paths)?;
|
|
116
|
-
} else if path.is_file() {
|
|
117
|
-
if let Ok(relative) = path.strip_prefix(root) {
|
|
118
|
-
paths.push(relative.to_string_lossy().replace('\\', "/"));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
Ok(())
|
|
123
|
-
}
|
|
124
|
-
|
|
125
80
|
fn untracked_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
126
81
|
let output = Command::new("git")
|
|
127
82
|
.args(["ls-files", "--others", "--exclude-standard", "-z"])
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::models::NaomeError;
|
|
6
|
+
|
|
7
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
8
|
+
pub(crate) enum RepoPathFallback {
|
|
9
|
+
Empty,
|
|
10
|
+
Filesystem,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub(crate) fn collect_tracked_and_untracked_paths(
|
|
14
|
+
root: &Path,
|
|
15
|
+
fallback: RepoPathFallback,
|
|
16
|
+
) -> Result<Vec<String>, NaomeError> {
|
|
17
|
+
let output = Command::new("git")
|
|
18
|
+
.args([
|
|
19
|
+
"ls-files",
|
|
20
|
+
"-z",
|
|
21
|
+
"--cached",
|
|
22
|
+
"--others",
|
|
23
|
+
"--exclude-standard",
|
|
24
|
+
])
|
|
25
|
+
.current_dir(root)
|
|
26
|
+
.output();
|
|
27
|
+
|
|
28
|
+
if let Ok(output) = output {
|
|
29
|
+
if output.status.success() {
|
|
30
|
+
return Ok(sorted_unique_paths(
|
|
31
|
+
output
|
|
32
|
+
.stdout
|
|
33
|
+
.split(|byte| *byte == 0)
|
|
34
|
+
.filter(|entry| !entry.is_empty())
|
|
35
|
+
.map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
|
|
36
|
+
.collect(),
|
|
37
|
+
));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
match fallback {
|
|
42
|
+
RepoPathFallback::Empty => Ok(Vec::new()),
|
|
43
|
+
RepoPathFallback::Filesystem => collect_filesystem_paths(root),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn collect_filesystem_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
48
|
+
let mut paths = Vec::new();
|
|
49
|
+
collect_files_recursive(root, root, &mut paths)?;
|
|
50
|
+
Ok(sorted_unique_paths(paths))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn collect_files_recursive(
|
|
54
|
+
root: &Path,
|
|
55
|
+
dir: &Path,
|
|
56
|
+
paths: &mut Vec<String>,
|
|
57
|
+
) -> Result<(), NaomeError> {
|
|
58
|
+
for entry in fs::read_dir(dir)? {
|
|
59
|
+
let entry = entry?;
|
|
60
|
+
let path = entry.path();
|
|
61
|
+
if path.is_dir() {
|
|
62
|
+
collect_files_recursive(root, &path, paths)?;
|
|
63
|
+
} else if path.is_file() {
|
|
64
|
+
if let Ok(relative) = path.strip_prefix(root) {
|
|
65
|
+
paths.push(relative.to_string_lossy().replace('\\', "/"));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
Ok(())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn sorted_unique_paths(mut paths: Vec<String>) -> Vec<String> {
|
|
73
|
+
paths.sort();
|
|
74
|
+
paths.dedup();
|
|
75
|
+
paths
|
|
76
|
+
}
|
|
@@ -1,32 +1,11 @@
|
|
|
1
1
|
use std::collections::BTreeSet;
|
|
2
2
|
use std::path::Path;
|
|
3
|
-
use std::process::Command;
|
|
4
3
|
|
|
5
4
|
use crate::models::NaomeError;
|
|
5
|
+
use crate::repo_paths::{collect_tracked_and_untracked_paths, RepoPathFallback};
|
|
6
6
|
|
|
7
7
|
pub(super) fn collect_repo_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
8
|
-
|
|
9
|
-
.args([
|
|
10
|
-
"ls-files",
|
|
11
|
-
"-z",
|
|
12
|
-
"--cached",
|
|
13
|
-
"--others",
|
|
14
|
-
"--exclude-standard",
|
|
15
|
-
])
|
|
16
|
-
.current_dir(root)
|
|
17
|
-
.output()?;
|
|
18
|
-
if !output.status.success() {
|
|
19
|
-
return Ok(Vec::new());
|
|
20
|
-
}
|
|
21
|
-
let mut paths = output
|
|
22
|
-
.stdout
|
|
23
|
-
.split(|byte| *byte == 0)
|
|
24
|
-
.filter(|entry| !entry.is_empty())
|
|
25
|
-
.map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
|
|
26
|
-
.collect::<Vec<_>>();
|
|
27
|
-
paths.sort();
|
|
28
|
-
paths.dedup();
|
|
29
|
-
Ok(paths)
|
|
8
|
+
collect_tracked_and_untracked_paths(root, RepoPathFallback::Empty)
|
|
30
9
|
}
|
|
31
10
|
|
|
32
11
|
pub(super) fn evidence(paths: &[String], markers: &[&str]) -> Vec<String> {
|
|
@@ -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");
|
|
@@ -21,12 +21,18 @@ pub(super) fn validate_task_state_projection_is_current(
|
|
|
21
21
|
if !read::active_path(root).is_file() {
|
|
22
22
|
return Ok(());
|
|
23
23
|
}
|
|
24
|
+
if super::local_runtime_ledger_is_ignored(root) {
|
|
25
|
+
return Ok(());
|
|
26
|
+
}
|
|
24
27
|
let Some(legacy) = read::read_legacy_task_state(root)? else {
|
|
25
28
|
return Ok(());
|
|
26
29
|
};
|
|
27
30
|
let Some(rendered) = render::render_from_ledger(root)? else {
|
|
28
31
|
return Ok(());
|
|
29
32
|
};
|
|
33
|
+
if super::legacy_completed_projection_covers_local_runtime_ledger(&rendered.state, &legacy) {
|
|
34
|
+
return Ok(());
|
|
35
|
+
}
|
|
30
36
|
if legacy != rendered.state {
|
|
31
37
|
errors.push(".naome/task-state.json is stale relative to .naome/tasks/. Run node .naome/bin/naome.js task render-state --write --json before completion or commit.".to_string());
|
|
32
38
|
}
|