@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
package/Cargo.lock
CHANGED
|
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
76
76
|
|
|
77
77
|
[[package]]
|
|
78
78
|
name = "naome-cli"
|
|
79
|
-
version = "1.3.
|
|
79
|
+
version = "1.3.8"
|
|
80
80
|
dependencies = [
|
|
81
81
|
"naome-core",
|
|
82
82
|
"serde_json",
|
|
@@ -84,7 +84,7 @@ dependencies = [
|
|
|
84
84
|
|
|
85
85
|
[[package]]
|
|
86
86
|
name = "naome-core"
|
|
87
|
-
version = "1.3.
|
|
87
|
+
version = "1.3.8"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
package/README.md
CHANGED
|
@@ -41,6 +41,11 @@ For agent-driven work, route the user's request through the harness:
|
|
|
41
41
|
naome route --prompt-file /path/to/prompt.txt --execute --json
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Prompt files should start with a fenced `naome-prompt-envelope-v1` JSON
|
|
45
|
+
envelope that normalizes the raw user text into canonical routing fields. A raw
|
|
46
|
+
natural-language prompt routes to a non-mutating normalization decision instead
|
|
47
|
+
of becoming a task by keyword inference.
|
|
48
|
+
|
|
44
49
|
## Why NAOME?
|
|
45
50
|
|
|
46
51
|
- Keeps agents inside explicit task scope.
|
|
@@ -84,8 +89,9 @@ naome commit -m "type(scope): summary"
|
|
|
84
89
|
|
|
85
90
|
`naome sync` installs or repairs the local harness files. It does not run a
|
|
86
91
|
hidden full-repository quality scan. It also migrates any active legacy
|
|
87
|
-
task-state into the task ledger automatically
|
|
88
|
-
|
|
92
|
+
task-state into the local task ledger automatically and untracks local
|
|
93
|
+
`.naome/tasks/` runtime folders from Git. If quality policy is newly seeded,
|
|
94
|
+
run `naome quality init --baseline` deliberately; use `--deep` or
|
|
89
95
|
`--deep-baseline` only when you want expensive repository-wide checks.
|
|
90
96
|
|
|
91
97
|
## Repository Docs
|
|
@@ -111,7 +117,7 @@ The main local policy files are:
|
|
|
111
117
|
quality policy.
|
|
112
118
|
- `.naome/repository-structure.json` for path role, module, and directory
|
|
113
119
|
structure policy.
|
|
114
|
-
- `.naome/task-state.json` for active task
|
|
120
|
+
- `.naome/task-state.json` for the compact committed active task projection.
|
|
115
121
|
|
|
116
122
|
Product defaults stay generic. Repository-specific policy belongs in the local
|
|
117
123
|
`.naome/` config files.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
use std::path::Path;
|
|
1
|
+
use std::path::{Component, Path};
|
|
2
2
|
|
|
3
|
+
use serde::Deserialize;
|
|
4
|
+
|
|
5
|
+
use crate::intent::prompt_envelope_json;
|
|
3
6
|
use crate::{git, models::NaomeError};
|
|
4
7
|
|
|
5
8
|
use super::helpers::{capsule_for_paths, context_item, is_source_path, normalize_paths};
|
|
@@ -7,6 +10,13 @@ use super::types::{ContextBudgetLedger, ContextItem, ContextSelection};
|
|
|
7
10
|
|
|
8
11
|
const MAX_CONTEXT_FILES: usize = 12;
|
|
9
12
|
|
|
13
|
+
#[derive(Debug, Deserialize)]
|
|
14
|
+
#[serde(rename_all = "camelCase")]
|
|
15
|
+
struct PromptEnvelopeContext {
|
|
16
|
+
schema: Option<String>,
|
|
17
|
+
referenced_paths: Option<Vec<String>>,
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
pub fn select_context_for_changed_paths(root: &Path) -> Result<ContextSelection, NaomeError> {
|
|
11
21
|
let paths = git::changed_paths(root)?;
|
|
12
22
|
Ok(selection_for_paths(root, "changed", paths))
|
|
@@ -19,7 +29,9 @@ pub fn select_context_for_prompt(
|
|
|
19
29
|
Ok(selection_for_paths(
|
|
20
30
|
root,
|
|
21
31
|
"prompt",
|
|
22
|
-
|
|
32
|
+
envelope_referenced_paths(prompt.as_ref())
|
|
33
|
+
.filter(|paths| !paths.is_empty())
|
|
34
|
+
.unwrap_or_else(|| mentioned_paths(root, prompt.as_ref())),
|
|
23
35
|
))
|
|
24
36
|
}
|
|
25
37
|
|
|
@@ -119,7 +131,7 @@ fn forbidden_context() -> Vec<String> {
|
|
|
119
131
|
.collect()
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
fn mentioned_paths(prompt: &str) -> Vec<String> {
|
|
134
|
+
fn mentioned_paths(root: &Path, prompt: &str) -> Vec<String> {
|
|
123
135
|
prompt
|
|
124
136
|
.split_whitespace()
|
|
125
137
|
.map(|token| {
|
|
@@ -127,8 +139,50 @@ fn mentioned_paths(prompt: &str) -> Vec<String> {
|
|
|
127
139
|
matches!(ch, '"' | '\'' | '`' | ',' | ';' | ':' | ')' | '(')
|
|
128
140
|
})
|
|
129
141
|
})
|
|
130
|
-
.filter(|token| token.contains('/') || is_source_path(token) || token.ends_with(".md"))
|
|
131
142
|
.filter(|token| !token.starts_with('-') && !token.starts_with("http"))
|
|
132
143
|
.map(|token| token.trim_start_matches("./").replace('\\', "/"))
|
|
144
|
+
.filter(|path| path_looks_contextual(path))
|
|
145
|
+
.filter(|path| path_exists_or_has_repo_parent(root, path))
|
|
133
146
|
.collect()
|
|
134
147
|
}
|
|
148
|
+
|
|
149
|
+
fn envelope_referenced_paths(prompt: &str) -> Option<Vec<String>> {
|
|
150
|
+
let input =
|
|
151
|
+
serde_json::from_str::<PromptEnvelopeContext>(prompt_envelope_json(prompt)?).ok()?;
|
|
152
|
+
if input.schema.as_deref() != Some("naome.prompt-envelope.v1") {
|
|
153
|
+
return None;
|
|
154
|
+
}
|
|
155
|
+
Some(
|
|
156
|
+
input
|
|
157
|
+
.referenced_paths
|
|
158
|
+
.unwrap_or_default()
|
|
159
|
+
.into_iter()
|
|
160
|
+
.map(|path| path.trim_start_matches("./").replace('\\', "/"))
|
|
161
|
+
.filter(|path| is_repo_relative_path(path))
|
|
162
|
+
.filter(|path| path_looks_contextual(path))
|
|
163
|
+
.collect(),
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn is_repo_relative_path(path: &str) -> bool {
|
|
168
|
+
let candidate = Path::new(path);
|
|
169
|
+
!path.contains(':')
|
|
170
|
+
&& !candidate.is_absolute()
|
|
171
|
+
&& !candidate
|
|
172
|
+
.components()
|
|
173
|
+
.any(|component| matches!(component, Component::ParentDir))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn path_looks_contextual(path: &str) -> bool {
|
|
177
|
+
path.contains('/') || is_source_path(path) || path.ends_with(".md")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn path_exists_or_has_repo_parent(root: &Path, path: &str) -> bool {
|
|
181
|
+
let candidate = root.join(path);
|
|
182
|
+
if candidate.exists() {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
candidate
|
|
186
|
+
.parent()
|
|
187
|
+
.is_some_and(|parent| parent != root && parent.exists())
|
|
188
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
use std::fs;
|
|
2
1
|
use std::path::Path;
|
|
3
2
|
use std::process::Command;
|
|
4
3
|
|
|
@@ -6,7 +5,7 @@ use crate::models::NaomeError;
|
|
|
6
5
|
use crate::paths;
|
|
7
6
|
|
|
8
7
|
pub fn changed_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
9
|
-
let ignored_patterns =
|
|
8
|
+
let ignored_patterns = paths::naomeignore_patterns(root);
|
|
10
9
|
let output = Command::new("git")
|
|
11
10
|
.args(["status", "--porcelain=v1", "-z", "-uall"])
|
|
12
11
|
.current_dir(root)
|
|
@@ -57,27 +56,3 @@ pub fn changed_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
|
57
56
|
paths.dedup();
|
|
58
57
|
Ok(paths)
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
fn read_naomeignore_patterns(root: &Path) -> Vec<String> {
|
|
62
|
-
let Ok(content) = fs::read_to_string(root.join(".naomeignore")) else {
|
|
63
|
-
return Vec::new();
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
let mut patterns = content
|
|
67
|
-
.lines()
|
|
68
|
-
.map(str::trim)
|
|
69
|
-
.filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
|
|
70
|
-
.map(normalize_ignore_pattern)
|
|
71
|
-
.collect::<Vec<_>>();
|
|
72
|
-
patterns.push(".naome/cache/**".to_string());
|
|
73
|
-
patterns
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
fn normalize_ignore_pattern(pattern: &str) -> String {
|
|
77
|
-
let normalized = pattern.trim_start_matches("./").replace('\\', "/");
|
|
78
|
-
if normalized.ends_with('/') {
|
|
79
|
-
format!("{normalized}**")
|
|
80
|
-
} else {
|
|
81
|
-
normalized
|
|
82
|
-
}
|
|
83
|
-
}
|
|
@@ -36,12 +36,6 @@ const CLASSES: &[(&str, &str, &str, &str)] = &[
|
|
|
36
36
|
"commit",
|
|
37
37
|
"restore_from_repository_or_package",
|
|
38
38
|
),
|
|
39
|
-
(
|
|
40
|
-
"durable_task_ledger",
|
|
41
|
-
"Committed task intent, scope, and lifecycle records.",
|
|
42
|
-
"commit",
|
|
43
|
-
"restore_from_repository",
|
|
44
|
-
),
|
|
45
39
|
(
|
|
46
40
|
"generated_projection",
|
|
47
41
|
"Compatibility views generated from durable state.",
|
|
@@ -67,23 +61,18 @@ const CLASSES: &[(&str, &str, &str, &str)] = &[
|
|
|
67
61
|
"restore_from_repository",
|
|
68
62
|
),
|
|
69
63
|
];
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
RuleSeed { path_pattern: ".naome/manifest.json", class: "durable_project_state" },
|
|
83
|
-
RuleSeed { path_pattern: ".naome/verification.json", class: "durable_project_state" },
|
|
84
|
-
RuleSeed { path_pattern: ".naome/repository-quality*.json", class: "durable_project_state" },
|
|
85
|
-
RuleSeed { path_pattern: "docs/naome/**", class: "durable_project_state" },
|
|
86
|
-
RuleSeed { path_pattern: "packages/naome/templates/naome-root/**", class: "durable_project_state" },
|
|
64
|
+
const RULES: &[(&str, &str)] = &[
|
|
65
|
+
(".naome/tmp/**", "local_runtime_state"),
|
|
66
|
+
(".naome/task-state.json", "generated_projection"),
|
|
67
|
+
(".naome/tasks/**", "local_runtime_state"),
|
|
68
|
+
(".naome/manifest.json", "durable_project_state"),
|
|
69
|
+
(".naome/verification.json", "durable_project_state"),
|
|
70
|
+
(".naome/repository-quality*.json", "durable_project_state"),
|
|
71
|
+
("docs/naome/**", "durable_project_state"),
|
|
72
|
+
(
|
|
73
|
+
"packages/naome/templates/naome-root/**",
|
|
74
|
+
"durable_project_state",
|
|
75
|
+
),
|
|
87
76
|
];
|
|
88
77
|
pub fn information_architecture_report() -> InformationArchitectureReport {
|
|
89
78
|
InformationArchitectureReport {
|
|
@@ -95,7 +84,7 @@ pub fn information_architecture_report() -> InformationArchitectureReport {
|
|
|
95
84
|
.collect(),
|
|
96
85
|
rules: RULES
|
|
97
86
|
.iter()
|
|
98
|
-
.map(|
|
|
87
|
+
.map(|(path_pattern, class)| rule_from_class(path_pattern, class))
|
|
99
88
|
.collect(),
|
|
100
89
|
}
|
|
101
90
|
}
|
|
@@ -104,8 +93,8 @@ pub fn classify_information_path(path: &str) -> InformationPathClassification {
|
|
|
104
93
|
let path = normalize_path(path);
|
|
105
94
|
let class_id = RULES
|
|
106
95
|
.iter()
|
|
107
|
-
.find(|
|
|
108
|
-
.map(|
|
|
96
|
+
.find(|(pattern, _)| matches_information_rule(&path, pattern))
|
|
97
|
+
.map(|(_, class)| *class)
|
|
109
98
|
.unwrap_or("product_source");
|
|
110
99
|
let class = class_by_id(class_id);
|
|
111
100
|
InformationPathClassification {
|
|
@@ -81,11 +81,13 @@ pub fn install_plan(harness_version: impl Into<String>) -> InstallPlan {
|
|
|
81
81
|
".naome/bin/naome-rust*",
|
|
82
82
|
".naome/cache/",
|
|
83
83
|
".naome/task-journal.jsonl",
|
|
84
|
+
".naome/tasks/",
|
|
84
85
|
];
|
|
85
86
|
git_exclude_entries.extend_from_slice(LOCAL_ONLY_MACHINE_OWNED_PATHS);
|
|
86
87
|
|
|
87
88
|
let mut git_untrack_paths = LOCAL_ONLY_MACHINE_OWNED_PATHS.to_vec();
|
|
88
89
|
git_untrack_paths.extend_from_slice(LOCAL_NATIVE_BINARY_PATHS);
|
|
90
|
+
git_untrack_paths.push(".naome/tasks");
|
|
89
91
|
|
|
90
92
|
InstallPlan {
|
|
91
93
|
schema: "naome.install-plan.v1",
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 >=
|
|
124
|
+
.filter(|candidate| candidate.confidence >= 90)
|
|
135
125
|
.filter(|candidate| {
|
|
136
126
|
!matches!(
|
|
137
127
|
candidate.kind,
|
|
138
|
-
IntentKind::NewTask
|
|
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
|