@lamentis/naome 1.0.2 → 1.1.0
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 +8 -1
- package/bin/naome-node.js +3 -1
- package/bin/naome.js +198 -3
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/main.rs +110 -13
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/decision.rs +82 -11
- package/crates/naome-core/src/git.rs +12 -1
- package/crates/naome-core/src/harness_health.rs +3 -1
- package/crates/naome-core/src/install_plan.rs +4 -2
- package/crates/naome-core/src/intent.rs +914 -0
- package/crates/naome-core/src/journal.rs +169 -0
- package/crates/naome-core/src/lib.rs +10 -1
- package/crates/naome-core/src/models.rs +63 -4
- package/crates/naome-core/src/route.rs +1000 -0
- package/crates/naome-core/src/task_state.rs +326 -21
- package/crates/naome-core/tests/decision.rs +8 -6
- package/crates/naome-core/tests/install_plan.rs +9 -1
- package/crates/naome-core/tests/intent.rs +826 -0
- package/crates/naome-core/tests/route.rs +1108 -0
- package/crates/naome-core/tests/task_state.rs +63 -4
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +7 -6
- package/templates/naome-root/.naome/bin/check-task-state.js +7 -6
- package/templates/naome-root/.naome/bin/naome.js +143 -13
- package/templates/naome-root/.naome/manifest.json +8 -7
- package/templates/naome-root/.naome/upgrade-state.json +1 -1
- package/templates/naome-root/AGENTS.md +30 -5
- package/templates/naome-root/docs/naome/agent-workflow.md +45 -24
- package/templates/naome-root/docs/naome/execution.md +55 -51
- package/templates/naome-root/docs/naome/index.md +10 -3
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
use std::fs::{self, OpenOptions};
|
|
2
|
+
use std::io::Write;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
use std::process::Command;
|
|
5
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
6
|
+
|
|
7
|
+
use serde::Serialize;
|
|
8
|
+
use serde_json::Value;
|
|
9
|
+
|
|
10
|
+
use crate::models::NaomeError;
|
|
11
|
+
|
|
12
|
+
const JOURNAL_PATH: &str = ".naome/task-journal.jsonl";
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
|
15
|
+
#[serde(rename_all = "camelCase")]
|
|
16
|
+
pub struct TaskJournalEntry {
|
|
17
|
+
pub schema: String,
|
|
18
|
+
pub recorded_at: String,
|
|
19
|
+
pub task_id: Option<String>,
|
|
20
|
+
pub request: Option<String>,
|
|
21
|
+
pub user_prompt: Option<String>,
|
|
22
|
+
pub revision_count: usize,
|
|
23
|
+
pub proof_summary: Vec<String>,
|
|
24
|
+
pub outcome: String,
|
|
25
|
+
pub commit_before: Option<String>,
|
|
26
|
+
pub commit_after: Option<String>,
|
|
27
|
+
pub head: Option<String>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn append_task_journal(
|
|
31
|
+
root: &Path,
|
|
32
|
+
outcome: &str,
|
|
33
|
+
commit_before: Option<String>,
|
|
34
|
+
commit_after: Option<String>,
|
|
35
|
+
) -> Result<Option<TaskJournalEntry>, NaomeError> {
|
|
36
|
+
ensure_journal_is_local_only(root)?;
|
|
37
|
+
let task_state = read_json(root, ".naome/task-state.json")?;
|
|
38
|
+
let Some(active_task) = task_state
|
|
39
|
+
.get("activeTask")
|
|
40
|
+
.filter(|value| !value.is_null())
|
|
41
|
+
else {
|
|
42
|
+
return Ok(None);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let entry = TaskJournalEntry {
|
|
46
|
+
schema: "naome.task-journal-entry.v1".to_string(),
|
|
47
|
+
recorded_at: timestamp_now(),
|
|
48
|
+
task_id: string_at(active_task, "id"),
|
|
49
|
+
request: string_at(active_task, "request"),
|
|
50
|
+
user_prompt: active_task
|
|
51
|
+
.get("userPrompt")
|
|
52
|
+
.and_then(|prompt| prompt.get("text"))
|
|
53
|
+
.and_then(Value::as_str)
|
|
54
|
+
.map(ToString::to_string),
|
|
55
|
+
revision_count: active_task
|
|
56
|
+
.get("revisions")
|
|
57
|
+
.and_then(Value::as_array)
|
|
58
|
+
.map_or(0, Vec::len),
|
|
59
|
+
proof_summary: proof_summary(active_task),
|
|
60
|
+
outcome: outcome.to_string(),
|
|
61
|
+
commit_before,
|
|
62
|
+
commit_after: commit_after.clone(),
|
|
63
|
+
head: git_head(root).ok().flatten(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if journal_contains(root, &entry)? {
|
|
67
|
+
return Ok(Some(entry));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fs::create_dir_all(root.join(".naome"))?;
|
|
71
|
+
let mut file = OpenOptions::new()
|
|
72
|
+
.create(true)
|
|
73
|
+
.append(true)
|
|
74
|
+
.open(root.join(JOURNAL_PATH))?;
|
|
75
|
+
writeln!(file, "{}", serde_json::to_string(&entry)?)?;
|
|
76
|
+
Ok(Some(entry))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn ensure_journal_is_local_only(root: &Path) -> Result<(), NaomeError> {
|
|
80
|
+
let git_info = root.join(".git").join("info");
|
|
81
|
+
if !git_info.is_dir() {
|
|
82
|
+
return Ok(());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let exclude_path = git_info.join("exclude");
|
|
86
|
+
let existing = fs::read_to_string(&exclude_path).unwrap_or_default();
|
|
87
|
+
if existing
|
|
88
|
+
.lines()
|
|
89
|
+
.map(str::trim)
|
|
90
|
+
.any(|line| line == JOURNAL_PATH)
|
|
91
|
+
{
|
|
92
|
+
return Ok(());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let mut next = existing;
|
|
96
|
+
if !next.is_empty() && !next.ends_with('\n') {
|
|
97
|
+
next.push('\n');
|
|
98
|
+
}
|
|
99
|
+
next.push_str("# NAOME local task journal.\n");
|
|
100
|
+
next.push_str(JOURNAL_PATH);
|
|
101
|
+
next.push('\n');
|
|
102
|
+
fs::write(exclude_path, next)?;
|
|
103
|
+
Ok(())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn journal_contains(root: &Path, entry: &TaskJournalEntry) -> Result<bool, NaomeError> {
|
|
107
|
+
let path = root.join(JOURNAL_PATH);
|
|
108
|
+
if !path.is_file() {
|
|
109
|
+
return Ok(false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let content = fs::read_to_string(path)?;
|
|
113
|
+
Ok(content.lines().any(|line| {
|
|
114
|
+
let Ok(value) = serde_json::from_str::<Value>(line) else {
|
|
115
|
+
return false;
|
|
116
|
+
};
|
|
117
|
+
value.get("taskId").and_then(Value::as_str) == entry.task_id.as_deref()
|
|
118
|
+
&& value.get("outcome").and_then(Value::as_str) == Some(entry.outcome.as_str())
|
|
119
|
+
&& value.get("commitAfter").and_then(Value::as_str) == entry.commit_after.as_deref()
|
|
120
|
+
}))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn proof_summary(active_task: &Value) -> Vec<String> {
|
|
124
|
+
active_task
|
|
125
|
+
.get("proofResults")
|
|
126
|
+
.and_then(Value::as_array)
|
|
127
|
+
.map(|proofs| {
|
|
128
|
+
proofs
|
|
129
|
+
.iter()
|
|
130
|
+
.filter_map(|proof| proof.get("checkId").and_then(Value::as_str))
|
|
131
|
+
.map(ToString::to_string)
|
|
132
|
+
.collect()
|
|
133
|
+
})
|
|
134
|
+
.unwrap_or_default()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn read_json(root: &Path, relative_path: &str) -> Result<Value, NaomeError> {
|
|
138
|
+
Ok(serde_json::from_str(&fs::read_to_string(
|
|
139
|
+
root.join(relative_path),
|
|
140
|
+
)?)?)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn string_at(value: &Value, key: &str) -> Option<String> {
|
|
144
|
+
value
|
|
145
|
+
.get(key)
|
|
146
|
+
.and_then(Value::as_str)
|
|
147
|
+
.map(ToString::to_string)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn git_head(root: &Path) -> Result<Option<String>, NaomeError> {
|
|
151
|
+
let output = Command::new("git")
|
|
152
|
+
.args(["rev-parse", "HEAD"])
|
|
153
|
+
.current_dir(root)
|
|
154
|
+
.output()?;
|
|
155
|
+
if !output.status.success() {
|
|
156
|
+
return Ok(None);
|
|
157
|
+
}
|
|
158
|
+
Ok(Some(
|
|
159
|
+
String::from_utf8_lossy(&output.stdout).trim().to_string(),
|
|
160
|
+
))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn timestamp_now() -> String {
|
|
164
|
+
let seconds = SystemTime::now()
|
|
165
|
+
.duration_since(UNIX_EPOCH)
|
|
166
|
+
.map(|duration| duration.as_secs())
|
|
167
|
+
.unwrap_or(0);
|
|
168
|
+
format!("unix:{seconds}")
|
|
169
|
+
}
|
|
@@ -2,8 +2,11 @@ mod decision;
|
|
|
2
2
|
mod git;
|
|
3
3
|
mod harness_health;
|
|
4
4
|
mod install_plan;
|
|
5
|
+
mod intent;
|
|
6
|
+
mod journal;
|
|
5
7
|
mod models;
|
|
6
8
|
mod paths;
|
|
9
|
+
mod route;
|
|
7
10
|
mod task_state;
|
|
8
11
|
mod verification;
|
|
9
12
|
mod verification_contract;
|
|
@@ -11,7 +14,13 @@ mod verification_contract;
|
|
|
11
14
|
pub use decision::{evaluate_decision, format_decision, EvaluationOptions};
|
|
12
15
|
pub use harness_health::{validate_harness_health, HarnessHealthOptions};
|
|
13
16
|
pub use install_plan::{install_plan, InstallPlan};
|
|
17
|
+
pub use intent::{evaluate_intent, format_intent, IntentDecision, PromptEvidence};
|
|
18
|
+
pub use journal::{append_task_journal, TaskJournalEntry};
|
|
14
19
|
pub use models::{Decision, NaomeError};
|
|
15
|
-
pub use
|
|
20
|
+
pub use route::{evaluate_route, explain_route, ExplainDecision, RouteDecision, RouteOptions};
|
|
21
|
+
pub use task_state::{
|
|
22
|
+
completed_task_commit_paths, validate_task_state, TaskStateMode, TaskStateOptions,
|
|
23
|
+
TaskStateReport,
|
|
24
|
+
};
|
|
16
25
|
pub use verification::seed_builtin_verification_checks;
|
|
17
26
|
pub use verification_contract::validate_verification_contract;
|
|
@@ -12,6 +12,9 @@ pub struct Decision {
|
|
|
12
12
|
pub summary: String,
|
|
13
13
|
pub allowed_actions: Vec<String>,
|
|
14
14
|
pub next_action: String,
|
|
15
|
+
pub user_message: String,
|
|
16
|
+
pub human_options: Vec<String>,
|
|
17
|
+
pub internal_notes: Vec<String>,
|
|
15
18
|
pub changed_paths: Vec<String>,
|
|
16
19
|
pub required_context: Vec<String>,
|
|
17
20
|
pub task: Option<TaskDecision>,
|
|
@@ -27,16 +30,23 @@ impl Decision {
|
|
|
27
30
|
allowed_actions: Vec<&str>,
|
|
28
31
|
next_action: &str,
|
|
29
32
|
) -> Self {
|
|
33
|
+
let allowed_actions: Vec<String> = allowed_actions
|
|
34
|
+
.into_iter()
|
|
35
|
+
.map(ToString::to_string)
|
|
36
|
+
.collect();
|
|
37
|
+
let (user_message, human_options, internal_notes) =
|
|
38
|
+
response_policy(state, blocked, summary, next_action, &allowed_actions);
|
|
39
|
+
|
|
30
40
|
Self {
|
|
31
41
|
schema: "naome.decision.v1".to_string(),
|
|
32
42
|
state: state.to_string(),
|
|
33
43
|
blocked,
|
|
34
44
|
summary: summary.to_string(),
|
|
35
|
-
allowed_actions
|
|
36
|
-
.into_iter()
|
|
37
|
-
.map(ToString::to_string)
|
|
38
|
-
.collect(),
|
|
45
|
+
allowed_actions,
|
|
39
46
|
next_action: next_action.to_string(),
|
|
47
|
+
user_message,
|
|
48
|
+
human_options,
|
|
49
|
+
internal_notes,
|
|
40
50
|
changed_paths: Vec::new(),
|
|
41
51
|
required_context: Vec::new(),
|
|
42
52
|
task: None,
|
|
@@ -46,6 +56,55 @@ impl Decision {
|
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
fn response_policy(
|
|
60
|
+
state: &str,
|
|
61
|
+
blocked: bool,
|
|
62
|
+
summary: &str,
|
|
63
|
+
next_action: &str,
|
|
64
|
+
allowed_actions: &[String],
|
|
65
|
+
) -> (String, Vec<String>, Vec<String>) {
|
|
66
|
+
let mut internal_notes = vec![format!("internal_state:{state}")];
|
|
67
|
+
if !allowed_actions.is_empty() {
|
|
68
|
+
internal_notes.push(format!(
|
|
69
|
+
"internal_allowed_actions:{}",
|
|
70
|
+
allowed_actions.join(",")
|
|
71
|
+
));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let human_options = if blocked && human_decision_required(state) {
|
|
75
|
+
allowed_actions.to_vec()
|
|
76
|
+
} else {
|
|
77
|
+
Vec::new()
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let user_message = match state {
|
|
81
|
+
"completed_task_unbaselined" => "The completed task is verified and waiting for the next request. If the next request is a separate task, NAOME can baseline this task automatically first.".to_string(),
|
|
82
|
+
"install_or_upgrade_unbaselined" | "harness_repair_unbaselined" => {
|
|
83
|
+
"NAOME setup or repair changes are ready to be routed. A separate new task can trigger an automatic baseline when policy allows.".to_string()
|
|
84
|
+
}
|
|
85
|
+
"ready_for_task" => "NAOME is ready for the next task.".to_string(),
|
|
86
|
+
"harness_unhealthy" => "NAOME harness health failed, so normal task work is blocked until the harness is repaired or reviewed.".to_string(),
|
|
87
|
+
_ => {
|
|
88
|
+
if blocked {
|
|
89
|
+
format!("{summary} {next_action}")
|
|
90
|
+
} else {
|
|
91
|
+
summary.to_string()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
(user_message, human_options, internal_notes)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn human_decision_required(state: &str) -> bool {
|
|
100
|
+
!matches!(
|
|
101
|
+
state,
|
|
102
|
+
"completed_task_unbaselined"
|
|
103
|
+
| "install_or_upgrade_unbaselined"
|
|
104
|
+
| "harness_repair_unbaselined"
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
49
108
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
|
50
109
|
#[serde(rename_all = "camelCase")]
|
|
51
110
|
pub struct TaskDecision {
|