@lamentis/naome 1.2.1 → 1.3.1

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 (155) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +117 -47
  3. package/bin/naome.js +65 -12
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/context_commands.rs +47 -0
  6. package/crates/naome-cli/src/dispatcher.rs +12 -2
  7. package/crates/naome-cli/src/main.rs +78 -29
  8. package/crates/naome-cli/src/quality_commands.rs +238 -34
  9. package/crates/naome-cli/src/quality_output.rs +34 -0
  10. package/crates/naome-cli/src/quality_reconcile_command.rs +45 -0
  11. package/crates/naome-cli/src/repository_model_commands.rs +84 -0
  12. package/crates/naome-cli/src/task_commands.rs +62 -0
  13. package/crates/naome-cli/src/workflow_commands.rs +120 -3
  14. package/crates/naome-core/Cargo.toml +1 -1
  15. package/crates/naome-core/src/context/helpers.rs +75 -0
  16. package/crates/naome-core/src/context/select.rs +134 -0
  17. package/crates/naome-core/src/context/types.rs +43 -0
  18. package/crates/naome-core/src/context.rs +6 -0
  19. package/crates/naome-core/src/decision/states.rs +1 -1
  20. package/crates/naome-core/src/decision.rs +4 -1
  21. package/crates/naome-core/src/git.rs +4 -2
  22. package/crates/naome-core/src/install_plan.rs +20 -0
  23. package/crates/naome-core/src/journal.rs +2 -7
  24. package/crates/naome-core/src/lib.rs +35 -8
  25. package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
  26. package/crates/naome-core/src/quality/adapter_support.rs +67 -0
  27. package/crates/naome-core/src/quality/adapters.rs +81 -18
  28. package/crates/naome-core/src/quality/baseline.rs +8 -0
  29. package/crates/naome-core/src/quality/cache.rs +151 -0
  30. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +19 -8
  31. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  32. package/crates/naome-core/src/quality/checks.rs +7 -8
  33. package/crates/naome-core/src/quality/cleanup.rs +36 -3
  34. package/crates/naome-core/src/quality/config.rs +21 -3
  35. package/crates/naome-core/src/quality/mod.rs +189 -10
  36. package/crates/naome-core/src/quality/reconcile.rs +138 -0
  37. package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
  38. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  39. package/crates/naome-core/src/quality/scanner/analysis.rs +175 -0
  40. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  41. package/crates/naome-core/src/quality/scanner.rs +235 -217
  42. package/crates/naome-core/src/quality/semantic/checks.rs +151 -0
  43. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  44. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  45. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  46. package/crates/naome-core/src/quality/semantic.rs +68 -0
  47. package/crates/naome-core/src/quality/structure/adapter_ios.rs +149 -0
  48. package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
  49. package/crates/naome-core/src/quality/structure/checks/directory.rs +13 -21
  50. package/crates/naome-core/src/quality/structure/checks.rs +1 -1
  51. package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
  52. package/crates/naome-core/src/quality/structure/classify.rs +52 -0
  53. package/crates/naome-core/src/quality/structure/config.rs +24 -3
  54. package/crates/naome-core/src/quality/structure/mod.rs +5 -2
  55. package/crates/naome-core/src/quality/structure/model.rs +8 -1
  56. package/crates/naome-core/src/quality/types.rs +59 -2
  57. package/crates/naome-core/src/repository_model/detect.rs +188 -0
  58. package/crates/naome-core/src/repository_model/explain.rs +121 -0
  59. package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
  60. package/crates/naome-core/src/repository_model/path_support.rs +59 -0
  61. package/crates/naome-core/src/repository_model/types.rs +152 -0
  62. package/crates/naome-core/src/repository_model/world.rs +48 -0
  63. package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
  64. package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
  65. package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
  66. package/crates/naome-core/src/repository_model.rs +164 -0
  67. package/crates/naome-core/src/route/builtin_checks.rs +41 -16
  68. package/crates/naome-core/src/task_ledger/import.rs +142 -0
  69. package/crates/naome-core/src/task_ledger/model.rs +13 -0
  70. package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
  71. package/crates/naome-core/src/task_ledger/read.rs +118 -0
  72. package/crates/naome-core/src/task_ledger/render.rs +55 -0
  73. package/crates/naome-core/src/task_ledger/write.rs +38 -0
  74. package/crates/naome-core/src/task_ledger.rs +48 -0
  75. package/crates/naome-core/src/task_state/api.rs +4 -2
  76. package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
  77. package/crates/naome-core/src/task_state/diff.rs +2 -2
  78. package/crates/naome-core/src/task_state/evidence.rs +8 -3
  79. package/crates/naome-core/src/task_state/mod.rs +1 -1
  80. package/crates/naome-core/src/task_state/progress.rs +13 -0
  81. package/crates/naome-core/src/task_state/proof_model.rs +8 -8
  82. package/crates/naome-core/src/task_state/repair.rs +2 -2
  83. package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
  84. package/crates/naome-core/src/task_state/types.rs +24 -0
  85. package/crates/naome-core/src/verification.rs +29 -18
  86. package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
  87. package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
  88. package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
  89. package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
  90. package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
  91. package/crates/naome-core/src/workflow/agent/support.rs +58 -0
  92. package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
  93. package/crates/naome-core/src/workflow/agent.rs +34 -0
  94. package/crates/naome-core/src/workflow/agent_types.rs +105 -0
  95. package/crates/naome-core/src/workflow/doctor.rs +183 -0
  96. package/crates/naome-core/src/workflow/mod.rs +13 -0
  97. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  98. package/crates/naome-core/src/workflow/output.rs +8 -2
  99. package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
  100. package/crates/naome-core/tests/context.rs +99 -0
  101. package/crates/naome-core/tests/harness_health.rs +4 -0
  102. package/crates/naome-core/tests/install_plan.rs +14 -0
  103. package/crates/naome-core/tests/quality.rs +190 -5
  104. package/crates/naome-core/tests/quality_performance.rs +268 -0
  105. package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
  106. package/crates/naome-core/tests/quality_structure_policy.rs +19 -0
  107. package/crates/naome-core/tests/repo_support/mod.rs +5 -1
  108. package/crates/naome-core/tests/repo_support/verification_values.rs +55 -0
  109. package/crates/naome-core/tests/repository_model.rs +281 -0
  110. package/crates/naome-core/tests/route_user_diff.rs +59 -7
  111. package/crates/naome-core/tests/semantic_legacy.rs +174 -0
  112. package/crates/naome-core/tests/task_ledger.rs +328 -0
  113. package/crates/naome-core/tests/task_state.rs +28 -0
  114. package/crates/naome-core/tests/verification.rs +29 -36
  115. package/crates/naome-core/tests/workflow_agent.rs +233 -0
  116. package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
  117. package/crates/naome-core/tests/workflow_doctor.rs +45 -0
  118. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  119. package/installer/codex-hooks.js +121 -0
  120. package/installer/context.js +10 -0
  121. package/installer/filesystem.js +4 -0
  122. package/installer/flows.js +8 -4
  123. package/installer/git-boundary.js +1 -0
  124. package/installer/harness-files.js +6 -0
  125. package/installer/install-plan.js +4 -0
  126. package/installer/main.js +1 -1
  127. package/installer/native.js +1 -1
  128. package/native/darwin-arm64/naome +0 -0
  129. package/native/linux-x64/naome +0 -0
  130. package/package.json +1 -1
  131. package/templates/naome-root/.codex/config.toml +2 -0
  132. package/templates/naome-root/.codex/hooks.json +70 -0
  133. package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
  134. package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
  135. package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
  136. package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
  137. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
  138. package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
  139. package/templates/naome-root/.naome/bin/naome.js +45 -7
  140. package/templates/naome-root/.naome/manifest.json +12 -6
  141. package/templates/naome-root/.naome/repository-model.json +6 -0
  142. package/templates/naome-root/.naome/repository-quality.json +3 -1
  143. package/templates/naome-root/.naome/verification.json +15 -1
  144. package/templates/naome-root/.naomeignore +1 -0
  145. package/templates/naome-root/AGENTS.md +38 -83
  146. package/templates/naome-root/docs/naome/agent-workflow.md +66 -28
  147. package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
  148. package/templates/naome-root/docs/naome/context-economy.md +73 -0
  149. package/templates/naome-root/docs/naome/first-run.md +25 -14
  150. package/templates/naome-root/docs/naome/index.md +18 -10
  151. package/templates/naome-root/docs/naome/repository-model.md +92 -0
  152. package/templates/naome-root/docs/naome/repository-quality.md +104 -5
  153. package/templates/naome-root/docs/naome/repository-structure.md +10 -3
  154. package/templates/naome-root/docs/naome/task-ledger.md +71 -0
  155. package/templates/naome-root/docs/naome/testing.md +16 -3
@@ -0,0 +1,142 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde_json::{json, Value};
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ use super::read::{active_path, task_dir};
9
+ use super::write::json_text;
10
+
11
+ pub(super) fn migrate_task_state_to_ledger(
12
+ root: &Path,
13
+ write_ledger: bool,
14
+ ) -> Result<Option<Value>, NaomeError> {
15
+ if active_path(root).is_file() {
16
+ return Ok(Some(json!({
17
+ "schema": "naome.task-ledger-migration.v1",
18
+ "source": "ledger",
19
+ "updated": false,
20
+ "reason": "ledger already exists"
21
+ })));
22
+ }
23
+
24
+ let Some(task_state) = super::read::read_legacy_task_state(root)? else {
25
+ return Ok(None);
26
+ };
27
+ let Some(active_task) = task_state
28
+ .get("activeTask")
29
+ .filter(|value| !value.is_null())
30
+ else {
31
+ return Ok(None);
32
+ };
33
+ let Some(task_id) = active_task.get("id").and_then(Value::as_str) else {
34
+ return Err(NaomeError::new(
35
+ "Cannot migrate task-state to ledger: activeTask.id is missing.",
36
+ ));
37
+ };
38
+
39
+ let task_dir = task_dir(root, task_id);
40
+ let proof_dir = task_dir.join("proofs");
41
+ let proofs = super::proof_record::expanded_proofs(active_task, root)?;
42
+ let status = task_state
43
+ .get("status")
44
+ .and_then(Value::as_str)
45
+ .unwrap_or("implementing");
46
+ let updated_at = task_state
47
+ .get("updatedAt")
48
+ .and_then(Value::as_str)
49
+ .unwrap_or("1970-01-01T00:00:00.000Z");
50
+
51
+ let active = json!({
52
+ "schema": "naome.task-ledger-active.v1",
53
+ "version": 1,
54
+ "primaryTaskId": task_id,
55
+ "worklanes": [
56
+ { "id": "default", "taskId": task_id, "status": "active" }
57
+ ]
58
+ });
59
+ let spec = task_spec(active_task);
60
+ let events = task_events(status, updated_at, task_state.get("blocker"));
61
+
62
+ if write_ledger {
63
+ fs::create_dir_all(&proof_dir)?;
64
+ fs::write(active_path(root), json_text(&active)?)?;
65
+ fs::write(task_dir.join("task.json"), json_text(&spec)?)?;
66
+ fs::write(task_dir.join("events.jsonl"), events)?;
67
+ for proof in &proofs {
68
+ let file_name = super::proof_record::proof_file_name(
69
+ proof
70
+ .get("checkId")
71
+ .and_then(Value::as_str)
72
+ .unwrap_or("proof"),
73
+ );
74
+ fs::write(proof_dir.join(file_name), json_text(proof)?)?;
75
+ }
76
+ if let Some(projection) = super::render::render_from_ledger(root)? {
77
+ super::write::write_task_state_projection(root, &projection.state)?;
78
+ }
79
+ }
80
+
81
+ Ok(Some(json!({
82
+ "schema": "naome.task-ledger-migration.v1",
83
+ "source": "task-state",
84
+ "updated": write_ledger,
85
+ "taskId": task_id,
86
+ "proofCount": proofs.len()
87
+ })))
88
+ }
89
+
90
+ fn task_spec(active_task: &Value) -> Value {
91
+ json!({
92
+ "schema": "naome.task-ledger-task.v1",
93
+ "version": 1,
94
+ "id": active_task.get("id").cloned().unwrap_or(Value::Null),
95
+ "request": active_task.get("request").cloned().unwrap_or(Value::Null),
96
+ "userPrompt": active_task.get("userPrompt").cloned().unwrap_or(Value::Null),
97
+ "admission": active_task.get("admission").cloned().unwrap_or(Value::Null),
98
+ "allowedPaths": active_task.get("allowedPaths").cloned().unwrap_or_else(|| json!([])),
99
+ "declaredChangeTypes": active_task
100
+ .get("declaredChangeTypes")
101
+ .cloned()
102
+ .unwrap_or_else(|| json!([])),
103
+ "requiredCheckIds": active_task
104
+ .get("requiredCheckIds")
105
+ .cloned()
106
+ .unwrap_or_else(|| json!([])),
107
+ "revisions": active_task.get("revisions").cloned().unwrap_or_else(|| json!([])),
108
+ "humanReview": active_task.get("humanReview").cloned().unwrap_or_else(|| {
109
+ json!({
110
+ "required": false,
111
+ "approved": false,
112
+ "reason": null
113
+ })
114
+ })
115
+ })
116
+ }
117
+
118
+ fn task_events(status: &str, updated_at: &str, blocker: Option<&Value>) -> String {
119
+ let mut events = Vec::new();
120
+ events.push(json!({
121
+ "schema": "naome.task-ledger-event.v1",
122
+ "type": "status",
123
+ "status": status,
124
+ "recordedAt": updated_at
125
+ }));
126
+ if let Some(blocker) = blocker.filter(|value| !value.is_null()) {
127
+ events.push(json!({
128
+ "schema": "naome.task-ledger-event.v1",
129
+ "type": "blocker",
130
+ "blocker": blocker,
131
+ "recordedAt": updated_at
132
+ }));
133
+ }
134
+
135
+ let mut text = events
136
+ .into_iter()
137
+ .map(|event| serde_json::to_string(&event).unwrap_or_else(|_| "{}".to_string()))
138
+ .collect::<Vec<_>>()
139
+ .join("\n");
140
+ text.push('\n');
141
+ text
142
+ }
@@ -0,0 +1,13 @@
1
+ use serde_json::Value;
2
+
3
+ #[derive(Debug, Clone, PartialEq, Eq)]
4
+ pub struct TaskLedgerProjection {
5
+ pub state: Value,
6
+ pub status: TaskLedgerStatus,
7
+ }
8
+
9
+ #[derive(Debug, Clone, PartialEq, Eq)]
10
+ pub enum TaskLedgerStatus {
11
+ Active,
12
+ LegacyFallback,
13
+ }
@@ -0,0 +1,52 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::{json, Value};
4
+
5
+ use crate::models::NaomeError;
6
+ use crate::task_state::canonical_proofs;
7
+
8
+ pub(super) fn expanded_proofs(active_task: &Value, root: &Path) -> Result<Vec<Value>, NaomeError> {
9
+ let mut errors = Vec::new();
10
+ let proofs = canonical_proofs(active_task, root, &mut errors)?;
11
+ if !errors.is_empty() {
12
+ return Err(NaomeError::new(format!(
13
+ "Cannot migrate invalid task proof into ledger: {}",
14
+ errors.join("; ")
15
+ )));
16
+ }
17
+
18
+ Ok(proofs
19
+ .into_iter()
20
+ .map(|proof| {
21
+ json!({
22
+ "schema": "naome.task-ledger-proof.v1",
23
+ "version": 1,
24
+ "taskId": active_task.get("id").cloned().unwrap_or(Value::Null),
25
+ "checkId": proof.check_id,
26
+ "command": proof.command,
27
+ "cwd": proof.cwd,
28
+ "exitCode": proof.exit_code,
29
+ "checkedAt": proof.checked_at,
30
+ "evidence": proof.evidence
31
+ })
32
+ })
33
+ .collect())
34
+ }
35
+
36
+ pub(super) fn proof_file_name(check_id: &str) -> String {
37
+ let mut name = check_id
38
+ .chars()
39
+ .map(|ch| {
40
+ if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
41
+ ch
42
+ } else {
43
+ '_'
44
+ }
45
+ })
46
+ .collect::<String>();
47
+ if name.is_empty() {
48
+ name = "proof".to_string();
49
+ }
50
+ name.push_str(".json");
51
+ name
52
+ }
@@ -0,0 +1,118 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use serde_json::{json, Value};
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ pub(super) fn read_legacy_task_state(root: &Path) -> Result<Option<Value>, NaomeError> {
9
+ let path = root.join(".naome/task-state.json");
10
+ if !path.is_file() {
11
+ return Ok(None);
12
+ }
13
+ let content = fs::read_to_string(path)?;
14
+ Ok(Some(serde_json::from_str(&content)?))
15
+ }
16
+
17
+ pub(super) fn read_active_task_id(root: &Path) -> Result<Option<String>, NaomeError> {
18
+ let path = active_path(root);
19
+ if !path.is_file() {
20
+ return Ok(None);
21
+ }
22
+ let active = read_json(&path)?;
23
+ Ok(active
24
+ .get("primaryTaskId")
25
+ .and_then(Value::as_str)
26
+ .map(ToString::to_string))
27
+ }
28
+
29
+ pub(super) fn read_task_spec(root: &Path, task_id: &str) -> Result<Value, NaomeError> {
30
+ read_json(&task_dir(root, task_id).join("task.json"))
31
+ }
32
+
33
+ pub(super) fn read_status(
34
+ root: &Path,
35
+ task_id: &str,
36
+ ) -> Result<(String, Option<Value>, String), NaomeError> {
37
+ let events_path = task_dir(root, task_id).join("events.jsonl");
38
+ let mut status = "implementing".to_string();
39
+ let mut blocker = None;
40
+ let mut updated_at = None;
41
+
42
+ if events_path.is_file() {
43
+ for line in fs::read_to_string(events_path)?.lines() {
44
+ if line.trim().is_empty() {
45
+ continue;
46
+ }
47
+ let event: Value = serde_json::from_str(line)?;
48
+ if let Some(recorded_at) = event.get("recordedAt").and_then(Value::as_str) {
49
+ updated_at = Some(recorded_at.to_string());
50
+ }
51
+ match event.get("type").and_then(Value::as_str) {
52
+ Some("status") => {
53
+ if let Some(next_status) = event.get("status").and_then(Value::as_str) {
54
+ status = next_status.to_string();
55
+ }
56
+ }
57
+ Some("blocker") => blocker = event.get("blocker").cloned(),
58
+ _ => {}
59
+ }
60
+ }
61
+ }
62
+
63
+ Ok((
64
+ status,
65
+ blocker,
66
+ updated_at.unwrap_or_else(|| "1970-01-01T00:00:00.000Z".to_string()),
67
+ ))
68
+ }
69
+
70
+ pub(super) fn read_proofs(root: &Path, task_id: &str) -> Result<Vec<Value>, NaomeError> {
71
+ let proofs_dir = task_dir(root, task_id).join("proofs");
72
+ if !proofs_dir.is_dir() {
73
+ return Ok(Vec::new());
74
+ }
75
+
76
+ let mut entries: Vec<PathBuf> = fs::read_dir(proofs_dir)?
77
+ .filter_map(Result::ok)
78
+ .map(|entry| entry.path())
79
+ .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("json"))
80
+ .collect();
81
+ entries.sort();
82
+
83
+ let mut proofs = Vec::new();
84
+ for path in entries {
85
+ let proof = read_json(&path)?;
86
+ proofs.push(json!({
87
+ "checkId": proof.get("checkId").cloned().unwrap_or(Value::Null),
88
+ "command": proof.get("command").cloned().unwrap_or(Value::Null),
89
+ "cwd": proof.get("cwd").cloned().unwrap_or(Value::Null),
90
+ "exitCode": proof.get("exitCode").cloned().unwrap_or(Value::Null),
91
+ "checkedAt": proof.get("checkedAt").cloned().unwrap_or(Value::Null),
92
+ "evidence": proof.get("evidence").cloned().unwrap_or_else(|| json!([])),
93
+ "outputSummary": proof.get("outputSummary").cloned().unwrap_or(Value::Null)
94
+ }));
95
+ }
96
+ Ok(proofs
97
+ .into_iter()
98
+ .map(|mut proof| {
99
+ if proof.get("outputSummary").is_some_and(Value::is_null) {
100
+ proof.as_object_mut().unwrap().remove("outputSummary");
101
+ }
102
+ proof
103
+ })
104
+ .collect())
105
+ }
106
+
107
+ fn read_json(path: &Path) -> Result<Value, NaomeError> {
108
+ let content = fs::read_to_string(path)?;
109
+ Ok(serde_json::from_str(&content)?)
110
+ }
111
+
112
+ pub(super) fn active_path(root: &Path) -> PathBuf {
113
+ root.join(".naome/tasks/active.json")
114
+ }
115
+
116
+ pub(super) fn task_dir(root: &Path, task_id: &str) -> PathBuf {
117
+ root.join(".naome/tasks").join(task_id)
118
+ }
@@ -0,0 +1,55 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::{json, Value};
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::model::{TaskLedgerProjection, TaskLedgerStatus};
8
+ use super::read::{read_active_task_id, read_proofs, read_status, read_task_spec};
9
+
10
+ pub(super) fn render_from_ledger(root: &Path) -> Result<Option<TaskLedgerProjection>, NaomeError> {
11
+ let Some(task_id) = read_active_task_id(root)? else {
12
+ return Ok(None);
13
+ };
14
+ let spec = read_task_spec(root, &task_id)?;
15
+ let (status, blocker, updated_at) = read_status(root, &task_id)?;
16
+ let active_task = render_active_task(spec, read_proofs(root, &task_id)?);
17
+ Ok(Some(TaskLedgerProjection {
18
+ state: json!({
19
+ "schema": "naome.task-state.v2",
20
+ "version": 2,
21
+ "status": status,
22
+ "activeTask": active_task,
23
+ "blocker": blocker,
24
+ "updatedAt": updated_at
25
+ }),
26
+ status: TaskLedgerStatus::Active,
27
+ }))
28
+ }
29
+
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")
39
+ .cloned()
40
+ .unwrap_or_else(|| json!([])),
41
+ "requiredCheckIds": spec
42
+ .get("requiredCheckIds")
43
+ .cloned()
44
+ .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(|| {
48
+ json!({
49
+ "required": false,
50
+ "approved": false,
51
+ "reason": null
52
+ })
53
+ })
54
+ })
55
+ }
@@ -0,0 +1,38 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ use super::read;
9
+ use super::render;
10
+
11
+ pub(super) fn write_task_state_projection(root: &Path, state: &Value) -> Result<(), NaomeError> {
12
+ let path = root.join(".naome/task-state.json");
13
+ fs::write(path, json_text(state)?)?;
14
+ Ok(())
15
+ }
16
+
17
+ pub(super) fn validate_task_state_projection_is_current(
18
+ root: &Path,
19
+ errors: &mut Vec<String>,
20
+ ) -> Result<(), NaomeError> {
21
+ if !read::active_path(root).is_file() {
22
+ return Ok(());
23
+ }
24
+ let Some(legacy) = read::read_legacy_task_state(root)? else {
25
+ return Ok(());
26
+ };
27
+ let Some(rendered) = render::render_from_ledger(root)? else {
28
+ return Ok(());
29
+ };
30
+ if legacy != rendered.state {
31
+ 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
+ }
33
+ Ok(())
34
+ }
35
+
36
+ pub(super) fn json_text(value: &Value) -> Result<String, NaomeError> {
37
+ Ok(format!("{}\n", serde_json::to_string_pretty(value)?))
38
+ }
@@ -0,0 +1,48 @@
1
+ mod import;
2
+ mod model;
3
+ mod proof_record;
4
+ mod read;
5
+ mod render;
6
+ mod write;
7
+
8
+ use std::path::Path;
9
+
10
+ use serde_json::Value;
11
+
12
+ use crate::models::NaomeError;
13
+
14
+ pub use model::{TaskLedgerProjection, TaskLedgerStatus};
15
+
16
+ pub fn migrate_task_state_to_ledger(
17
+ root: &Path,
18
+ write_ledger: bool,
19
+ ) -> Result<Option<Value>, NaomeError> {
20
+ import::migrate_task_state_to_ledger(root, write_ledger)
21
+ }
22
+
23
+ pub fn read_task_state_projection(root: &Path) -> Result<Option<Value>, NaomeError> {
24
+ if let Some(projection) = render::render_from_ledger(root)? {
25
+ return Ok(Some(projection.state));
26
+ }
27
+ read::read_legacy_task_state(root)
28
+ }
29
+
30
+ pub fn render_task_state_from_ledger(
31
+ root: &Path,
32
+ write_projection: bool,
33
+ ) -> Result<Option<Value>, NaomeError> {
34
+ let Some(projection) = render::render_from_ledger(root)? else {
35
+ return Ok(None);
36
+ };
37
+ if write_projection {
38
+ write::write_task_state_projection(root, &projection.state)?;
39
+ }
40
+ Ok(Some(projection.state))
41
+ }
42
+
43
+ pub(crate) fn validate_task_state_projection_is_current(
44
+ root: &Path,
45
+ errors: &mut Vec<String>,
46
+ ) -> Result<(), NaomeError> {
47
+ write::validate_task_state_projection_is_current(root, errors)
48
+ }
@@ -3,6 +3,7 @@ use std::path::Path;
3
3
  use serde_json::Value;
4
4
 
5
5
  use crate::models::NaomeError;
6
+ use crate::task_ledger::{read_task_state_projection, validate_task_state_projection_is_current};
6
7
 
7
8
  use super::completion::{
8
9
  validate_admission, validate_commit_gate, validate_complete_task, validate_progress,
@@ -16,7 +17,7 @@ use super::shape::{
16
17
  };
17
18
  use super::task_diff_api::validate_harness_health_gate;
18
19
  use super::types::*;
19
- use super::util::read_json;
20
+
20
21
  pub fn validate_task_state(
21
22
  root: &Path,
22
23
  options: TaskStateOptions,
@@ -25,10 +26,11 @@ pub fn validate_task_state(
25
26
  errors: Vec::new(),
26
27
  notices: Vec::new(),
27
28
  };
28
- let Some(task_state) = read_json(root, ".naome/task-state.json", &mut report.errors)? else {
29
+ let Some(task_state) = read_task_state_projection(root)? else {
29
30
  return Ok(report);
30
31
  };
31
32
 
33
+ validate_task_state_projection_is_current(root, &mut report.errors)?;
32
34
  validate_task_state_shape(&task_state, &mut report.errors);
33
35
  let status = task_state
34
36
  .get("status")
@@ -1,16 +1,15 @@
1
1
  use std::path::Path;
2
2
 
3
- use serde_json::Value;
4
-
5
3
  use crate::models::NaomeError;
6
4
 
7
5
  use super::api::validate_task_state;
8
6
  use super::git_io::read_git_changed_paths;
9
7
  use super::repair::is_safe_harness_refresh_path;
10
8
  use super::types::{
11
- CompletedTaskHarnessRefreshDiff, TaskStateMode, TaskStateOptions, CONTROL_STATE_PATH,
9
+ is_control_state_path, read_complete_active_task, CompletedTaskHarnessRefreshDiff,
10
+ TaskStateMode, TaskStateOptions,
12
11
  };
13
- use super::util::{matches_any_pattern, read_json, string_array};
12
+ use super::util::{matches_any_pattern, string_array};
14
13
  pub(super) fn add_completed_task_diff_notice(
15
14
  root: &Path,
16
15
  notices: &mut Vec<String>,
@@ -27,17 +26,7 @@ pub(super) fn add_completed_task_diff_notice(
27
26
  pub fn completed_task_harness_refresh_diff(
28
27
  root: &Path,
29
28
  ) -> Result<Option<CompletedTaskHarnessRefreshDiff>, NaomeError> {
30
- let mut read_errors = Vec::new();
31
- let Some(task_state) = read_json(root, ".naome/task-state.json", &mut read_errors)? else {
32
- return Ok(None);
33
- };
34
- if !read_errors.is_empty() {
35
- return Ok(None);
36
- }
37
- if task_state.get("status").and_then(Value::as_str) != Some("complete") {
38
- return Ok(None);
39
- }
40
- let Some(active_task) = task_state.get("activeTask") else {
29
+ let Some((_, active_task)) = read_complete_active_task(root)? else {
41
30
  return Ok(None);
42
31
  };
43
32
 
@@ -47,7 +36,7 @@ pub fn completed_task_harness_refresh_diff(
47
36
  let mut other_paths = Vec::new();
48
37
 
49
38
  for path in read_git_changed_paths(root)? {
50
- if path == CONTROL_STATE_PATH {
39
+ if is_control_state_path(&path) {
51
40
  continue;
52
41
  }
53
42
  if matches_any_pattern(&path, &allowed_paths) {
@@ -6,7 +6,7 @@ use serde_json::Value;
6
6
  use crate::models::NaomeError;
7
7
 
8
8
  use super::git_io::read_git_changed_entries;
9
- use super::types::{ChangedEntry, TaskDiff, CONTROL_STATE_PATH};
9
+ use super::types::{is_control_state_path, ChangedEntry, TaskDiff};
10
10
  use super::util::{matches_any_pattern, normalize_path, string_array};
11
11
  pub(super) fn validate_changed_paths(
12
12
  active_task: &Value,
@@ -80,7 +80,7 @@ pub(super) fn task_diff_from_entries(active_task: &Value, entries: &[ChangedEntr
80
80
  let diff_paths: Vec<String> = entries
81
81
  .iter()
82
82
  .map(|entry| entry.path.clone())
83
- .filter(|path| path != CONTROL_STATE_PATH)
83
+ .filter(|path| !is_control_state_path(path))
84
84
  .collect();
85
85
  let outside_paths = diff_paths
86
86
  .iter()
@@ -7,7 +7,7 @@ use crate::models::NaomeError;
7
7
 
8
8
  use super::deleted_paths::read_historical_deleted_paths;
9
9
  use super::git_io::read_git_changed_entries;
10
- use super::types::{ALLOWED_EVIDENCE_STATUS, CONTROL_STATE_PATH};
10
+ use super::types::{is_control_state_path, ALLOWED_EVIDENCE_STATUS, CONTROL_STATE_PATH};
11
11
  use super::util::{
12
12
  is_non_empty_string, matches_any_pattern, normalize_path, require_string, string_array,
13
13
  };
@@ -68,7 +68,12 @@ pub(super) fn validate_control_state_patterns(
68
68
  };
69
69
 
70
70
  for pattern in patterns {
71
- if matches_any_pattern(CONTROL_STATE_PATH, std::slice::from_ref(&pattern)) {
71
+ if matches_any_pattern(CONTROL_STATE_PATH, std::slice::from_ref(&pattern))
72
+ || matches_any_pattern(
73
+ ".naome/tasks/example/task.json",
74
+ std::slice::from_ref(&pattern),
75
+ )
76
+ {
72
77
  errors.push(format!(
73
78
  "{field_name} cannot include NAOME control state: {pattern}"
74
79
  ));
@@ -89,7 +94,7 @@ pub(super) fn validate_control_state_paths(
89
94
  let Some(path) = evidence_entry_path(entry) else {
90
95
  continue;
91
96
  };
92
- if normalize_path(&path) == CONTROL_STATE_PATH {
97
+ if is_control_state_path(&normalize_path(&path)) {
93
98
  errors.push(format!(
94
99
  "{field_name} cannot include NAOME control state: {path}"
95
100
  ));
@@ -30,7 +30,7 @@ mod util;
30
30
 
31
31
  pub use api::validate_task_state;
32
32
  pub use completed_refresh::completed_task_harness_refresh_diff;
33
- pub(crate) use proof_model::canonical_proof_check_ids;
33
+ pub(crate) use proof_model::{canonical_proof_check_ids, canonical_proofs};
34
34
  pub use task_diff_api::{
35
35
  completed_task_commit_diff, completed_task_commit_paths, harness_refresh_diff,
36
36
  harness_refresh_with_unrelated_diff,
@@ -3,6 +3,7 @@ use std::path::Path;
3
3
  use serde_json::Value;
4
4
 
5
5
  use crate::models::NaomeError;
6
+ use crate::repository_model_drift;
6
7
 
7
8
  use super::diff::validate_changed_paths;
8
9
  use super::git_io::read_git_changed_paths;
@@ -33,6 +34,7 @@ pub(super) fn validate_progress(
33
34
  if let Some(active_task) = task_state.get("activeTask") {
34
35
  validate_changed_paths(active_task, root, errors)?;
35
36
  }
37
+ validate_repository_model_current(root, errors)?;
36
38
  }
37
39
  "needs_human_review" => {
38
40
  validate_human_review_state(task_state, root, errors)?;
@@ -60,6 +62,17 @@ pub(super) fn validate_progress(
60
62
  Ok(())
61
63
  }
62
64
 
65
+ fn validate_repository_model_current(
66
+ root: &Path,
67
+ errors: &mut Vec<String>,
68
+ ) -> Result<(), NaomeError> {
69
+ let drift = repository_model_drift(root)?;
70
+ if drift.model_present && drift.stale {
71
+ errors.extend(drift.messages);
72
+ }
73
+ Ok(())
74
+ }
75
+
63
76
  pub(super) fn checked_status<'a>(
64
77
  task_state: &'a Value,
65
78
  root: &Path,
@@ -8,13 +8,13 @@ use super::compact_proof::compact_proofs;
8
8
  use super::proof_sources::{check_id_from_proof, read_verification_defaults};
9
9
 
10
10
  #[derive(Debug, Clone)]
11
- pub(super) struct CanonicalProof {
12
- pub(super) check_id: String,
13
- pub(super) command: String,
14
- pub(super) cwd: String,
15
- pub(super) exit_code: i64,
16
- pub(super) checked_at: String,
17
- pub(super) evidence: Vec<Value>,
11
+ pub(crate) struct CanonicalProof {
12
+ pub(crate) check_id: String,
13
+ pub(crate) command: String,
14
+ pub(crate) cwd: String,
15
+ pub(crate) exit_code: i64,
16
+ pub(crate) checked_at: String,
17
+ pub(crate) evidence: Vec<Value>,
18
18
  }
19
19
 
20
20
  #[derive(Debug, Clone)]
@@ -23,7 +23,7 @@ pub(super) struct VerificationDefaults {
23
23
  pub(super) cwd: String,
24
24
  }
25
25
 
26
- pub(super) fn canonical_proofs(
26
+ pub(crate) fn canonical_proofs(
27
27
  active_task: &Value,
28
28
  root: &Path,
29
29
  errors: &mut Vec<String>,