@lamentis/naome 1.3.3 → 1.3.5

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 (38) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +0 -5
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-cli/src/main.rs +1 -0
  5. package/crates/naome-cli/src/workflow_commands.rs +30 -4
  6. package/crates/naome-core/Cargo.toml +1 -1
  7. package/crates/naome-core/src/information_architecture.rs +179 -0
  8. package/crates/naome-core/src/install_plan.rs +0 -12
  9. package/crates/naome-core/src/lib.rs +5 -0
  10. package/crates/naome-core/src/route/git_ops.rs +50 -5
  11. package/crates/naome-core/src/task_ledger/render.rs +50 -16
  12. package/crates/naome-core/tests/information_architecture.rs +58 -0
  13. package/crates/naome-core/tests/install_plan.rs +0 -11
  14. package/crates/naome-core/tests/task_ledger.rs +62 -0
  15. package/installer/context.js +0 -10
  16. package/installer/filesystem.js +0 -4
  17. package/installer/flows.js +18 -4
  18. package/installer/harness-file-ops.js +4 -0
  19. package/installer/install-plan.js +0 -4
  20. package/native/darwin-arm64/naome +0 -0
  21. package/native/linux-x64/naome +0 -0
  22. package/package.json +1 -1
  23. package/templates/naome-root/.naome/bin/check-harness-health.js +3 -3
  24. package/templates/naome-root/.naome/bin/check-task-state.js +3 -3
  25. package/templates/naome-root/.naome/bin/naome.js +18 -47
  26. package/templates/naome-root/.naome/manifest.json +3 -3
  27. package/templates/naome-root/docs/naome/agent-workflow.md +7 -4
  28. package/templates/naome-root/docs/naome/architecture.md +11 -2
  29. package/templates/naome-root/docs/naome/task-ledger.md +13 -1
  30. package/templates/naome-root/docs/naome/testing.md +0 -4
  31. package/installer/codex-hooks.js +0 -121
  32. package/templates/naome-root/.codex/config.toml +0 -2
  33. package/templates/naome-root/.codex/hooks.json +0 -70
  34. package/templates/naome-root/.naome/bin/codex-hook-io.js +0 -122
  35. package/templates/naome-root/.naome/bin/codex-hook-policy.js +0 -180
  36. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +0 -174
  37. package/templates/naome-root/.naome/bin/codex-hook.js +0 -6
  38. package/templates/naome-root/docs/naome/codex-hooks.md +0 -82
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.3.3"
79
+ version = "1.3.5"
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.3"
87
+ version = "1.3.5"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
package/README.md CHANGED
@@ -50,8 +50,6 @@ naome route --prompt-file /path/to/prompt.txt --execute --json
50
50
  perfect on day one.
51
51
  - Records verification proof before a task can be treated as complete.
52
52
  - Keeps sync fast by making baseline and deep quality scans explicit.
53
- - Can install optional Codex hooks for earlier agent feedback without making
54
- hooks a human workflow requirement.
55
53
 
56
54
  ## Safety Model
57
55
 
@@ -99,8 +97,6 @@ After sync, NAOME writes the agent-facing workflow into `docs/naome/`:
99
97
  - `docs/naome/testing.md` maps change types to required checks.
100
98
  - `docs/naome/repository-quality.md` explains quality, structure, and cleanup
101
99
  policy.
102
- - `docs/naome/codex-hooks.md` explains the optional Codex hook acceleration
103
- layer.
104
100
 
105
101
  Agents should follow the repository's NAOME docs instead of guessing workflow
106
102
  rules from generic project files.
@@ -116,7 +112,6 @@ The main local policy files are:
116
112
  - `.naome/repository-structure.json` for path role, module, and directory
117
113
  structure policy.
118
114
  - `.naome/task-state.json` for active task state and proof.
119
- - `.codex/hooks.json` only when optional Codex hooks were explicitly enabled.
120
115
 
121
116
  Product defaults stay generic. Repository-specific policy belongs in the local
122
117
  `.naome/` config files.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.3.3"
3
+ version = "1.3.5"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -69,6 +69,7 @@ const HELP: &str = r#"Usage:
69
69
  naome workflow phases [--json]
70
70
  naome workflow processes [--json]
71
71
  naome workflow mutations --path <path> [path...] [--json]
72
+ naome workflow information [--path <path>] [--json]
72
73
  naome check-harness-health [--root <path>] [--allow-missing-archive]
73
74
  naome check-task-state [--root <path>] [--admission|--progress|--commit-gate|--push-gate] [--allow-missing-archive]
74
75
  naome validate-verification [--root <path>]"#;
@@ -1,9 +1,10 @@
1
1
  use std::path::Path;
2
2
 
3
3
  use naome_core::{
4
- agent_run_plan, classify_mutations, context_delta_report, decision_gate, doctor_report,
5
- edit_session_watchdog, proof_plan_for_task, refresh_integrity, repository_capability_graph,
6
- safe_rg_args, summarize_command_output, tracked_process_report, validate_search_command,
4
+ agent_run_plan, classify_information_path, classify_mutations, context_delta_report,
5
+ decision_gate, doctor_report, edit_session_watchdog, information_architecture_report,
6
+ proof_plan_for_task, refresh_integrity, repository_capability_graph, safe_rg_args,
7
+ summarize_command_output, tracked_process_report, validate_search_command,
7
8
  verification_phase_plan,
8
9
  };
9
10
 
@@ -32,7 +33,7 @@ pub fn run_workflow_command(
32
33
  args: &[String],
33
34
  ) -> Result<(), Box<dyn std::error::Error>> {
34
35
  let Some(subcommand) = args.get(1).map(String::as_str) else {
35
- return Err("naome workflow requires agent-plan, context-delta, proof-plan, capabilities, edit-watchdog, decision-gate, digest, search-profile, check-search, phases, processes, or mutations.".into());
36
+ return Err("naome workflow requires agent-plan, context-delta, proof-plan, capabilities, edit-watchdog, decision-gate, digest, search-profile, check-search, phases, processes, mutations, or information.".into());
36
37
  };
37
38
  let json = args.iter().any(|arg| arg == "--json");
38
39
 
@@ -103,6 +104,7 @@ pub fn run_workflow_command(
103
104
  }
104
105
  }
105
106
  "mutations" => run_mutations(root, args, json)?,
107
+ "information" => run_information(args, json)?,
106
108
  _ => return Err(format!("unknown naome workflow command: {subcommand}").into()),
107
109
  }
108
110
  Ok(())
@@ -179,6 +181,30 @@ fn run_mutations(
179
181
  Ok(())
180
182
  }
181
183
 
184
+ fn run_information(args: &[String], json: bool) -> Result<(), Box<dyn std::error::Error>> {
185
+ if let Some(path) = option_value(args, "--path") {
186
+ let classification = classify_information_path(&path);
187
+ if json {
188
+ println!("{}", serde_json::to_string_pretty(&classification)?);
189
+ } else {
190
+ println!(
191
+ "{}: {} ({}, {})",
192
+ classification.path,
193
+ classification.class,
194
+ classification.commit_policy,
195
+ classification.restore_policy
196
+ );
197
+ }
198
+ return Ok(());
199
+ }
200
+
201
+ print_json_or_label(
202
+ serde_json::to_value(information_architecture_report())?,
203
+ json,
204
+ "NAOME information architecture ready.",
205
+ )
206
+ }
207
+
182
208
  fn run_context_delta(
183
209
  root: &Path,
184
210
  args: &[String],
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.3.3"
3
+ version = "1.3.5"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -0,0 +1,179 @@
1
+ use serde::Serialize;
2
+
3
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4
+ pub struct InformationArchitectureReport {
5
+ pub schema: &'static str,
6
+ pub version: u8,
7
+ pub classes: Vec<InformationClass>,
8
+ pub rules: Vec<InformationPathRule>,
9
+ }
10
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11
+ pub struct InformationClass {
12
+ pub id: &'static str,
13
+ pub description: &'static str,
14
+ pub commit_policy: &'static str,
15
+ pub restore_policy: &'static str,
16
+ }
17
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18
+ pub struct InformationPathRule {
19
+ pub path_pattern: &'static str,
20
+ pub class: &'static str,
21
+ pub commit_policy: &'static str,
22
+ pub restore_policy: &'static str,
23
+ }
24
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25
+ pub struct InformationPathClassification {
26
+ pub path: String,
27
+ pub class: &'static str,
28
+ pub commit_policy: &'static str,
29
+ pub restore_policy: &'static str,
30
+ }
31
+
32
+ const CLASSES: &[(&str, &str, &str, &str)] = &[
33
+ (
34
+ "durable_project_state",
35
+ "Committed configuration, policy, and templates required to reconstruct the harness.",
36
+ "commit",
37
+ "restore_from_repository_or_package",
38
+ ),
39
+ (
40
+ "durable_task_ledger",
41
+ "Committed task intent, scope, and lifecycle records.",
42
+ "commit",
43
+ "restore_from_repository",
44
+ ),
45
+ (
46
+ "generated_projection",
47
+ "Compatibility views generated from durable state.",
48
+ "commit_when_required_by_compatibility",
49
+ "regenerate_from_durable_state",
50
+ ),
51
+ (
52
+ "local_runtime_state",
53
+ "Machine-local prompts, caches, temporary files, and runtime state.",
54
+ "never_commit",
55
+ "recreate_on_demand",
56
+ ),
57
+ (
58
+ "run_evidence",
59
+ "Verification outputs from a specific run.",
60
+ "prefer_compact_release_evidence",
61
+ "restore_from_ci_or_task_ledger_when_audited",
62
+ ),
63
+ (
64
+ "product_source",
65
+ "Repository source, package metadata, tests, scripts, and documentation.",
66
+ "commit_when_in_task_scope",
67
+ "restore_from_repository",
68
+ ),
69
+ ];
70
+ struct RuleSeed {
71
+ path_pattern: &'static str,
72
+ class: &'static str,
73
+ }
74
+
75
+ const RULES: &[RuleSeed] = &[
76
+ RuleSeed { path_pattern: ".naome/tmp/**", class: "local_runtime_state" },
77
+ RuleSeed { path_pattern: ".naome/task-state.json", class: "generated_projection" },
78
+ RuleSeed { path_pattern: ".naome/tasks/active.json", class: "durable_task_ledger" },
79
+ RuleSeed { path_pattern: ".naome/tasks/*/task.json", class: "durable_task_ledger" },
80
+ RuleSeed { path_pattern: ".naome/tasks/*/events.jsonl", class: "durable_task_ledger" },
81
+ RuleSeed { path_pattern: ".naome/tasks/*/proofs/*.json", class: "run_evidence" },
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" },
87
+ ];
88
+ pub fn information_architecture_report() -> InformationArchitectureReport {
89
+ InformationArchitectureReport {
90
+ schema: "naome.information-architecture.v1",
91
+ version: 1,
92
+ classes: CLASSES
93
+ .iter()
94
+ .map(|entry| class_from_tuple(*entry))
95
+ .collect(),
96
+ rules: RULES
97
+ .iter()
98
+ .map(|rule| rule_from_class(rule.path_pattern, rule.class))
99
+ .collect(),
100
+ }
101
+ }
102
+
103
+ pub fn classify_information_path(path: &str) -> InformationPathClassification {
104
+ let path = normalize_path(path);
105
+ let class_id = RULES
106
+ .iter()
107
+ .find(|rule| matches_information_rule(&path, rule.path_pattern))
108
+ .map(|rule| rule.class)
109
+ .unwrap_or("product_source");
110
+ let class = class_by_id(class_id);
111
+ InformationPathClassification {
112
+ path,
113
+ class: class.id,
114
+ commit_policy: class.commit_policy,
115
+ restore_policy: class.restore_policy,
116
+ }
117
+ }
118
+
119
+ fn rule_from_class(path_pattern: &'static str, class_id: &'static str) -> InformationPathRule {
120
+ let class = class_by_id(class_id);
121
+ InformationPathRule {
122
+ path_pattern,
123
+ class: class.id,
124
+ commit_policy: class.commit_policy,
125
+ restore_policy: class.restore_policy,
126
+ }
127
+ }
128
+
129
+ fn class_by_id(id: &str) -> InformationClass {
130
+ CLASSES
131
+ .iter()
132
+ .find(|(class_id, _, _, _)| *class_id == id)
133
+ .map(|entry| class_from_tuple(*entry))
134
+ .expect("information class rule must reference a known class")
135
+ }
136
+
137
+ fn class_from_tuple(
138
+ entry: (&'static str, &'static str, &'static str, &'static str),
139
+ ) -> InformationClass {
140
+ InformationClass {
141
+ id: entry.0,
142
+ description: entry.1,
143
+ commit_policy: entry.2,
144
+ restore_policy: entry.3,
145
+ }
146
+ }
147
+
148
+ fn normalize_path(path: &str) -> String {
149
+ path.replace('\\', "/")
150
+ .trim_start_matches("./")
151
+ .trim_end_matches('/')
152
+ .to_string()
153
+ }
154
+
155
+ fn matches_information_rule(path: &str, pattern: &str) -> bool {
156
+ if let Some(prefix) = pattern.strip_suffix("/**") {
157
+ return path == prefix || path.starts_with(&format!("{prefix}/"));
158
+ }
159
+ if let Some(prefix) = pattern
160
+ .strip_suffix("*.json")
161
+ .filter(|prefix| !prefix.contains('*'))
162
+ {
163
+ return path.starts_with(prefix) && path.ends_with(".json");
164
+ }
165
+
166
+ let path_parts: Vec<&str> = path.split('/').collect();
167
+ let pattern_parts: Vec<&str> = pattern.split('/').collect();
168
+ path_parts.len() == pattern_parts.len()
169
+ && path_parts
170
+ .iter()
171
+ .zip(pattern_parts.iter())
172
+ .all(|(path_part, pattern_part)| {
173
+ *pattern_part == "*"
174
+ || *path_part == *pattern_part
175
+ || pattern_part
176
+ .strip_prefix('*')
177
+ .is_some_and(|suffix| path_part.ends_with(suffix))
178
+ })
179
+ }
@@ -60,16 +60,6 @@ pub const LOCAL_NATIVE_BINARY_PATHS: &[&str] = &[
60
60
  ".naome/task-journal.jsonl",
61
61
  ];
62
62
 
63
- pub const OPTIONAL_CODEX_HOOK_PATHS: &[&str] = &[
64
- ".codex/config.toml",
65
- ".codex/hooks.json",
66
- ".naome/bin/codex-hook.js",
67
- ".naome/bin/codex-hook-io.js",
68
- ".naome/bin/codex-hook-policy.js",
69
- ".naome/bin/codex-hook-runtime.js",
70
- "docs/naome/codex-hooks.md",
71
- ];
72
-
73
63
  #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
74
64
  #[serde(rename_all = "camelCase")]
75
65
  pub struct InstallPlan {
@@ -78,7 +68,6 @@ pub struct InstallPlan {
78
68
  pub machine_owned: Vec<&'static str>,
79
69
  pub project_owned: Vec<&'static str>,
80
70
  pub local_only_machine_owned: Vec<&'static str>,
81
- pub optional_codex_hook_paths: Vec<&'static str>,
82
71
  pub gitignore_entries: Vec<&'static str>,
83
72
  pub git_exclude_entries: Vec<&'static str>,
84
73
  pub git_untrack_paths: Vec<&'static str>,
@@ -104,7 +93,6 @@ pub fn install_plan(harness_version: impl Into<String>) -> InstallPlan {
104
93
  machine_owned: MACHINE_OWNED_PATHS.to_vec(),
105
94
  project_owned: PROJECT_OWNED_PATHS.to_vec(),
106
95
  local_only_machine_owned: LOCAL_ONLY_MACHINE_OWNED_PATHS.to_vec(),
107
- optional_codex_hook_paths: OPTIONAL_CODEX_HOOK_PATHS.to_vec(),
108
96
  gitignore_entries,
109
97
  git_exclude_entries,
110
98
  git_untrack_paths,
@@ -2,6 +2,7 @@ mod context;
2
2
  mod decision;
3
3
  mod git;
4
4
  mod harness_health;
5
+ mod information_architecture;
5
6
  mod install_plan;
6
7
  mod intent;
7
8
  mod journal;
@@ -23,6 +24,10 @@ pub use context::{
23
24
  };
24
25
  pub use decision::{evaluate_decision, format_decision, EvaluationOptions};
25
26
  pub use harness_health::{validate_harness_health, HarnessHealthOptions};
27
+ pub use information_architecture::{
28
+ classify_information_path, information_architecture_report, InformationArchitectureReport,
29
+ InformationClass, InformationPathClassification, InformationPathRule,
30
+ };
26
31
  pub use install_plan::{install_plan, InstallPlan};
27
32
  pub use install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
28
33
  pub use intent::{evaluate_intent, format_intent, IntentDecision, PromptEvidence};
@@ -37,8 +37,25 @@ fn git_add_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
37
37
  if paths.is_empty() {
38
38
  return Ok(());
39
39
  }
40
+ let tracked_paths = git_ls_files(root, &[], paths)?;
41
+ if !tracked_paths.is_empty() {
42
+ let mut update_args = vec!["add", "-u", "--"];
43
+ update_args.extend(tracked_paths.iter().map(String::as_str));
44
+ ensure_git_success(
45
+ Command::new("git")
46
+ .args(update_args)
47
+ .current_dir(root)
48
+ .output()?,
49
+ )?;
50
+ }
51
+
52
+ let untracked_paths = git_ls_files(root, &["--others", "--exclude-standard"], paths)?;
53
+ if untracked_paths.is_empty() {
54
+ return Ok(());
55
+ }
56
+
40
57
  let mut args = vec!["add", "--"];
41
- args.extend(paths.iter().map(String::as_str));
58
+ args.extend(untracked_paths.iter().map(String::as_str));
42
59
  let output = Command::new("git").args(args).current_dir(root).output()?;
43
60
  if output.status.success() {
44
61
  Ok(())
@@ -47,6 +64,31 @@ fn git_add_paths(root: &Path, paths: &[String]) -> Result<(), NaomeError> {
47
64
  }
48
65
  }
49
66
 
67
+ fn git_ls_files(
68
+ root: &Path,
69
+ mode_args: &[&str],
70
+ paths: &[String],
71
+ ) -> Result<Vec<String>, NaomeError> {
72
+ let mut args = vec!["ls-files"];
73
+ args.extend(mode_args);
74
+ args.extend(["-z", "--"]);
75
+ args.extend(paths.iter().map(String::as_str));
76
+ let output = Command::new("git").args(args).current_dir(root).output()?;
77
+ if output.status.success() {
78
+ Ok(split_nul_paths(&output.stdout))
79
+ } else {
80
+ Err(NaomeError::new(command_output(&output)))
81
+ }
82
+ }
83
+
84
+ fn split_nul_paths(bytes: &[u8]) -> Vec<String> {
85
+ String::from_utf8_lossy(bytes)
86
+ .split('\0')
87
+ .filter(|path| !path.is_empty())
88
+ .map(ToString::to_string)
89
+ .collect()
90
+ }
91
+
50
92
  pub(super) fn git_commit(root: &Path, message: &str) -> Result<(), NaomeError> {
51
93
  ensure_git_success(git_output(root, &["commit", "-m", message])?)
52
94
  }
@@ -64,9 +106,12 @@ pub(super) fn ensure_git_success(output: Output) -> Result<(), NaomeError> {
64
106
  }
65
107
 
66
108
  pub(super) fn command_output(output: &std::process::Output) -> String {
67
- let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
68
- if !stderr.is_empty() {
69
- return stderr;
109
+ let stderr = String::from_utf8_lossy(&output.stderr);
110
+ let stdout = String::from_utf8_lossy(&output.stdout);
111
+ for message in [stderr.trim(), stdout.trim()] {
112
+ if !message.is_empty() {
113
+ return message.to_string();
114
+ }
70
115
  }
71
- String::from_utf8_lossy(&output.stdout).trim().to_string()
116
+ format!("git exited with status {}", output.status)
72
117
  }
@@ -1,6 +1,6 @@
1
1
  use std::path::Path;
2
2
 
3
- use serde_json::{json, Value};
3
+ use serde_json::{json, Map, Value};
4
4
 
5
5
  use crate::models::NaomeError;
6
6
 
@@ -28,28 +28,62 @@ pub(super) fn render_from_ledger(root: &Path) -> Result<Option<TaskLedgerProject
28
28
  }
29
29
 
30
30
  fn render_active_task(spec: Value, proof_results: Vec<Value>) -> Value {
31
- json!({
32
- "id": spec.get("id").cloned().unwrap_or(Value::Null),
33
- "request": spec.get("request").cloned().unwrap_or(Value::Null),
34
- "userPrompt": spec.get("userPrompt").cloned().unwrap_or(Value::Null),
35
- "admission": spec.get("admission").cloned().unwrap_or(Value::Null),
36
- "allowedPaths": spec.get("allowedPaths").cloned().unwrap_or_else(|| json!([])),
37
- "declaredChangeTypes": spec
38
- .get("declaredChangeTypes")
31
+ let mut active_task = Map::new();
32
+ active_task.insert(
33
+ "id".to_string(),
34
+ spec.get("id").cloned().unwrap_or(Value::Null),
35
+ );
36
+ active_task.insert(
37
+ "request".to_string(),
38
+ spec.get("request").cloned().unwrap_or(Value::Null),
39
+ );
40
+ active_task.insert(
41
+ "userPrompt".to_string(),
42
+ spec.get("userPrompt").cloned().unwrap_or(Value::Null),
43
+ );
44
+ active_task.insert(
45
+ "admission".to_string(),
46
+ spec.get("admission").cloned().unwrap_or(Value::Null),
47
+ );
48
+ active_task.insert(
49
+ "allowedPaths".to_string(),
50
+ spec.get("allowedPaths")
39
51
  .cloned()
40
52
  .unwrap_or_else(|| json!([])),
41
- "requiredCheckIds": spec
42
- .get("requiredCheckIds")
53
+ );
54
+ active_task.insert(
55
+ "declaredChangeTypes".to_string(),
56
+ spec.get("declaredChangeTypes")
43
57
  .cloned()
44
58
  .unwrap_or_else(|| json!([])),
45
- "proofResults": proof_results,
46
- "revisions": spec.get("revisions").cloned().unwrap_or_else(|| json!([])),
47
- "humanReview": spec.get("humanReview").cloned().unwrap_or_else(|| {
59
+ );
60
+ active_task.insert(
61
+ "requiredCheckIds".to_string(),
62
+ spec.get("requiredCheckIds")
63
+ .cloned()
64
+ .unwrap_or_else(|| json!([])),
65
+ );
66
+ active_task.insert("proofResults".to_string(), Value::Array(proof_results));
67
+ if let Some(path_sets) = spec.get("proofPathSets") {
68
+ active_task.insert("proofPathSets".to_string(), path_sets.clone());
69
+ }
70
+ if let Some(batches) = spec.get("proofBatches") {
71
+ active_task.insert("proofBatches".to_string(), batches.clone());
72
+ }
73
+ active_task.insert(
74
+ "revisions".to_string(),
75
+ spec.get("revisions").cloned().unwrap_or_else(|| json!([])),
76
+ );
77
+ active_task.insert(
78
+ "humanReview".to_string(),
79
+ spec.get("humanReview").cloned().unwrap_or_else(|| {
48
80
  json!({
49
81
  "required": false,
50
82
  "approved": false,
51
83
  "reason": null
52
84
  })
53
- })
54
- })
85
+ }),
86
+ );
87
+
88
+ Value::Object(active_task)
55
89
  }
@@ -0,0 +1,58 @@
1
+ use naome_core::{classify_information_path, information_architecture_report};
2
+
3
+ #[test]
4
+ fn classifies_naome_information_by_restore_source() {
5
+ let durable = classify_information_path(".naome/verification.json");
6
+ assert_eq!(durable.class, "durable_project_state");
7
+ assert_eq!(durable.commit_policy, "commit");
8
+ assert_eq!(durable.restore_policy, "restore_from_repository_or_package");
9
+
10
+ let projection = classify_information_path(".naome/task-state.json");
11
+ assert_eq!(projection.class, "generated_projection");
12
+ assert_eq!(
13
+ projection.commit_policy,
14
+ "commit_when_required_by_compatibility"
15
+ );
16
+ assert_eq!(projection.restore_policy, "regenerate_from_durable_state");
17
+
18
+ let local = classify_information_path(".naome/tmp/route.prompt");
19
+ assert_eq!(local.class, "local_runtime_state");
20
+ assert_eq!(local.commit_policy, "never_commit");
21
+ assert_eq!(local.restore_policy, "recreate_on_demand");
22
+
23
+ let proof = classify_information_path(".naome/tasks/readme-task/proofs/diff-check.json");
24
+ assert_eq!(proof.class, "run_evidence");
25
+ assert_eq!(proof.commit_policy, "prefer_compact_release_evidence");
26
+ assert_eq!(
27
+ proof.restore_policy,
28
+ "restore_from_ci_or_task_ledger_when_audited"
29
+ );
30
+
31
+ let task = classify_information_path(".naome/tasks/readme-task/task.json");
32
+ assert_eq!(task.class, "durable_task_ledger");
33
+ assert_eq!(task.commit_policy, "commit");
34
+ assert_eq!(task.restore_policy, "restore_from_repository");
35
+ }
36
+
37
+ #[test]
38
+ fn reports_information_architecture_contract_for_agents() {
39
+ let report = information_architecture_report();
40
+
41
+ assert_eq!(report.schema, "naome.information-architecture.v1");
42
+ assert!(report
43
+ .classes
44
+ .iter()
45
+ .any(|class| class.id == "durable_project_state"));
46
+ assert!(report
47
+ .classes
48
+ .iter()
49
+ .any(|class| class.id == "generated_projection"));
50
+ assert!(report
51
+ .classes
52
+ .iter()
53
+ .any(|class| class.id == "run_evidence"));
54
+ assert!(report
55
+ .rules
56
+ .iter()
57
+ .any(|rule| rule.path_pattern == ".naome/tasks/*/proofs/*.json"));
58
+ }
@@ -19,17 +19,6 @@ fn install_plan_marks_machine_docs_and_bins_local_only() {
19
19
  assert!(!plan
20
20
  .local_only_machine_owned
21
21
  .contains(&"docs/naome/architecture.md"));
22
- assert!(plan
23
- .optional_codex_hook_paths
24
- .contains(&".codex/hooks.json"));
25
- assert!(plan
26
- .optional_codex_hook_paths
27
- .contains(&".codex/config.toml"));
28
- assert!(plan
29
- .optional_codex_hook_paths
30
- .contains(&".naome/bin/codex-hook.js"));
31
- assert!(!plan.machine_owned.contains(&".naome/bin/codex-hook.js"));
32
- assert!(!plan.project_owned.contains(&".codex/hooks.json"));
33
22
  }
34
23
 
35
24
  #[test]
@@ -119,6 +119,57 @@ fn task_state_v2_can_migrate_to_ledger_without_losing_compact_proof_data() {
119
119
  assert!(report.errors.is_empty(), "{:?}", report.errors);
120
120
  }
121
121
 
122
+ #[test]
123
+ fn ledger_projection_preserves_compact_proof_batches_from_task_spec() {
124
+ let repo = TestRepo::new("compact-ledger");
125
+ repo.init_git();
126
+ repo.write_file("README.md", "# Baseline\n");
127
+ repo.write_base_harness(Some(idle_state()));
128
+ repo.git(&["add", "."]);
129
+ repo.git(&["commit", "-m", "baseline"]);
130
+ let head = repo.git_stdout(&["rev-parse", "HEAD"]);
131
+
132
+ repo.write_file(
133
+ ".naome/tasks/active.json",
134
+ &format_json(object([
135
+ ("schema", json!("naome.task-ledger-active.v1")),
136
+ ("version", json!(1)),
137
+ ("primaryTaskId", json!("readme-task")),
138
+ (
139
+ "worklanes",
140
+ json!([{ "id": "default", "taskId": "readme-task", "status": "active" }]),
141
+ ),
142
+ ])),
143
+ );
144
+ repo.write_file(
145
+ ".naome/tasks/readme-task/task.json",
146
+ &format_json(compact_task_spec_record(&head)),
147
+ );
148
+ repo.write_file(
149
+ ".naome/tasks/readme-task/events.jsonl",
150
+ concat!(
151
+ "{\"schema\":\"naome.task-ledger-event.v1\",\"type\":\"status\",\"status\":\"implementing\",\"recordedAt\":\"2026-05-08T00:00:01.000Z\"}\n",
152
+ "{\"schema\":\"naome.task-ledger-event.v1\",\"type\":\"status\",\"status\":\"complete\",\"recordedAt\":\"2026-05-08T00:00:02.000Z\"}\n"
153
+ ),
154
+ );
155
+ repo.write_file("README.md", "# Changed\n");
156
+
157
+ let projected = read_task_state_projection(repo.path()).unwrap().unwrap();
158
+ assert_eq!(projected["activeTask"]["proofResults"], json!([]));
159
+ assert_eq!(
160
+ projected["activeTask"]["proofPathSets"]["changed"],
161
+ json!(["README.md"])
162
+ );
163
+ assert_eq!(
164
+ projected["activeTask"]["proofBatches"][0]["proofs"][0]["checkId"],
165
+ "diff-check"
166
+ );
167
+
168
+ render_task_state_from_ledger(repo.path(), true).unwrap();
169
+ let report = validate_task_state(repo.path(), TaskStateOptions::default()).unwrap();
170
+ assert!(report.errors.is_empty(), "{:?}", report.errors);
171
+ }
172
+
122
173
  #[test]
123
174
  fn task_state_validation_reports_stale_rendered_projection_when_ledger_exists() {
124
175
  let repo = TestRepo::new("stale-projection");
@@ -261,6 +312,17 @@ fn task_spec_record(admission_head: &str) -> Value {
261
312
  ])
262
313
  }
263
314
 
315
+ fn compact_task_spec_record(admission_head: &str) -> Value {
316
+ let mut task = task_spec_record(admission_head);
317
+ let task_object = task.as_object_mut().unwrap();
318
+ task_object.insert(
319
+ "proofPathSets".to_string(),
320
+ object([("changed", json!(["README.md"]))]),
321
+ );
322
+ task_object.insert("proofBatches".to_string(), json!([compact_proof_batch()]));
323
+ task
324
+ }
325
+
264
326
  fn diff_proof_record() -> Value {
265
327
  object([
266
328
  ("schema", json!("naome.task-ledger-proof.v1")),