@lamentis/naome 1.3.0 → 1.3.2

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 (149) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +11 -2
  3. package/bin/naome.js +62 -24
  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 +6 -0
  7. package/crates/naome-cli/src/main.rs +43 -6
  8. package/crates/naome-cli/src/quality_commands.rs +31 -46
  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 +100 -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/install_plan.rs +18 -0
  22. package/crates/naome-core/src/intent/resolver_catalog/active.rs +38 -0
  23. package/crates/naome-core/src/intent/resolver_catalog/baseline.rs +44 -0
  24. package/crates/naome-core/src/intent/resolver_catalog/completed.rs +56 -0
  25. package/crates/naome-core/src/intent/resolver_catalog/dirty.rs +32 -0
  26. package/crates/naome-core/src/intent/resolver_catalog/ready.rs +32 -0
  27. package/crates/naome-core/src/intent/resolver_catalog/system.rs +20 -0
  28. package/crates/naome-core/src/intent/resolver_catalog.rs +12 -166
  29. package/crates/naome-core/src/journal.rs +2 -7
  30. package/crates/naome-core/src/lib.rs +33 -10
  31. package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
  32. package/crates/naome-core/src/quality/adapter_support.rs +67 -0
  33. package/crates/naome-core/src/quality/adapters.rs +81 -18
  34. package/crates/naome-core/src/quality/cache.rs +7 -9
  35. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +4 -7
  36. package/crates/naome-core/src/quality/config.rs +21 -3
  37. package/crates/naome-core/src/quality/mod.rs +138 -7
  38. package/crates/naome-core/src/quality/reconcile.rs +138 -0
  39. package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
  40. package/crates/naome-core/src/quality/scanner/analysis.rs +20 -5
  41. package/crates/naome-core/src/quality/scanner.rs +62 -17
  42. package/crates/naome-core/src/quality/semantic/checks.rs +17 -0
  43. package/crates/naome-core/src/quality/semantic/route.rs +1 -1
  44. package/crates/naome-core/src/quality/structure/adapter_ios.rs +149 -0
  45. package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
  46. package/crates/naome-core/src/quality/structure/checks/directory.rs +6 -4
  47. package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
  48. package/crates/naome-core/src/quality/structure/config.rs +24 -3
  49. package/crates/naome-core/src/quality/structure/mod.rs +3 -0
  50. package/crates/naome-core/src/quality/types.rs +20 -1
  51. package/crates/naome-core/src/repository_model/detect.rs +188 -0
  52. package/crates/naome-core/src/repository_model/explain.rs +121 -0
  53. package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
  54. package/crates/naome-core/src/repository_model/path_support.rs +59 -0
  55. package/crates/naome-core/src/repository_model/types.rs +152 -0
  56. package/crates/naome-core/src/repository_model/world.rs +48 -0
  57. package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
  58. package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
  59. package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
  60. package/crates/naome-core/src/repository_model.rs +164 -0
  61. package/crates/naome-core/src/route/builtin_checks.rs +40 -1
  62. package/crates/naome-core/src/task_ledger/import.rs +142 -0
  63. package/crates/naome-core/src/task_ledger/model.rs +13 -0
  64. package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
  65. package/crates/naome-core/src/task_ledger/read.rs +118 -0
  66. package/crates/naome-core/src/task_ledger/render.rs +55 -0
  67. package/crates/naome-core/src/task_ledger/write.rs +38 -0
  68. package/crates/naome-core/src/task_ledger.rs +48 -0
  69. package/crates/naome-core/src/task_state/api.rs +4 -2
  70. package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
  71. package/crates/naome-core/src/task_state/diff.rs +2 -2
  72. package/crates/naome-core/src/task_state/evidence.rs +8 -3
  73. package/crates/naome-core/src/task_state/mod.rs +1 -1
  74. package/crates/naome-core/src/task_state/progress.rs +13 -0
  75. package/crates/naome-core/src/task_state/proof_model.rs +8 -8
  76. package/crates/naome-core/src/task_state/repair.rs +2 -2
  77. package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
  78. package/crates/naome-core/src/task_state/types.rs +24 -0
  79. package/crates/naome-core/src/verification.rs +29 -18
  80. package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
  81. package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
  82. package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
  83. package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
  84. package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
  85. package/crates/naome-core/src/workflow/agent/support.rs +58 -0
  86. package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
  87. package/crates/naome-core/src/workflow/agent.rs +34 -0
  88. package/crates/naome-core/src/workflow/agent_types.rs +105 -0
  89. package/crates/naome-core/src/workflow/doctor.rs +39 -0
  90. package/crates/naome-core/src/workflow/mod.rs +11 -0
  91. package/crates/naome-core/src/workflow/output.rs +8 -2
  92. package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
  93. package/crates/naome-core/tests/context.rs +99 -0
  94. package/crates/naome-core/tests/harness_health.rs +33 -40
  95. package/crates/naome-core/tests/install_plan.rs +12 -0
  96. package/crates/naome-core/tests/quality.rs +178 -2
  97. package/crates/naome-core/tests/quality_performance.rs +39 -2
  98. package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
  99. package/crates/naome-core/tests/repo_support/mod.rs +7 -1
  100. package/crates/naome-core/tests/repo_support/verification_values.rs +148 -1
  101. package/crates/naome-core/tests/repository_model.rs +281 -0
  102. package/crates/naome-core/tests/route_user_diff.rs +49 -1
  103. package/crates/naome-core/tests/semantic_legacy.rs +72 -38
  104. package/crates/naome-core/tests/task_ledger.rs +328 -0
  105. package/crates/naome-core/tests/task_state.rs +34 -14
  106. package/crates/naome-core/tests/task_state_support/mod.rs +2 -1
  107. package/crates/naome-core/tests/task_state_support/states.rs +28 -11
  108. package/crates/naome-core/tests/verification.rs +14 -39
  109. package/crates/naome-core/tests/verification_contract.rs +6 -52
  110. package/crates/naome-core/tests/workflow_agent.rs +233 -0
  111. package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
  112. package/crates/naome-core/tests/workflow_doctor.rs +21 -0
  113. package/crates/naome-core/tests/workflow_integrity.rs +2 -20
  114. package/crates/naome-core/tests/workflow_support/mod.rs +59 -20
  115. package/installer/codex-hooks.js +121 -0
  116. package/installer/context.js +10 -0
  117. package/installer/filesystem.js +4 -0
  118. package/installer/flows.js +8 -4
  119. package/installer/harness-files.js +6 -0
  120. package/installer/install-plan.js +4 -0
  121. package/installer/main.js +1 -1
  122. package/installer/native.js +1 -1
  123. package/native/darwin-arm64/naome +0 -0
  124. package/native/linux-x64/naome +0 -0
  125. package/package.json +1 -1
  126. package/templates/naome-root/.codex/config.toml +2 -0
  127. package/templates/naome-root/.codex/hooks.json +70 -0
  128. package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
  129. package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
  130. package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
  131. package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
  132. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
  133. package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
  134. package/templates/naome-root/.naome/bin/naome.js +35 -4
  135. package/templates/naome-root/.naome/manifest.json +12 -6
  136. package/templates/naome-root/.naome/repository-model.json +6 -0
  137. package/templates/naome-root/.naome/repository-quality.json +3 -1
  138. package/templates/naome-root/.naome/verification.json +15 -1
  139. package/templates/naome-root/AGENTS.md +38 -83
  140. package/templates/naome-root/docs/naome/agent-workflow.md +54 -18
  141. package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
  142. package/templates/naome-root/docs/naome/context-economy.md +73 -0
  143. package/templates/naome-root/docs/naome/first-run.md +25 -14
  144. package/templates/naome-root/docs/naome/index.md +18 -10
  145. package/templates/naome-root/docs/naome/repository-model.md +92 -0
  146. package/templates/naome-root/docs/naome/repository-quality.md +47 -7
  147. package/templates/naome-root/docs/naome/repository-structure.md +10 -3
  148. package/templates/naome-root/docs/naome/task-ledger.md +71 -0
  149. package/templates/naome-root/docs/naome/testing.md +16 -3
@@ -0,0 +1,164 @@
1
+ mod detect;
2
+ mod explain;
3
+ mod path_scan;
4
+ mod path_support;
5
+ mod types;
6
+ mod world;
7
+ mod world_adapters;
8
+ mod world_path_facts;
9
+ mod world_paths;
10
+
11
+ use std::fs;
12
+ use std::path::Path;
13
+
14
+ use crate::models::NaomeError;
15
+
16
+ use detect::detect_repository_model;
17
+ use explain::explain_path;
18
+ pub use types::{
19
+ RepositoryEntity, RepositoryFact, RepositoryModel, RepositoryModelDrift,
20
+ RepositoryModelRefresh, RepositoryPathExplanation, RepositoryPathFact, RepositoryRoot,
21
+ RepositoryVerificationCheck, RepositoryWorldSignal,
22
+ };
23
+
24
+ const MODEL_PATH: &str = ".naome/repository-model.json";
25
+
26
+ pub fn refresh_repository_model(
27
+ root: &Path,
28
+ write: bool,
29
+ ) -> Result<RepositoryModelRefresh, NaomeError> {
30
+ let model = detect_repository_model(root)?;
31
+ let existing = read_existing_model(root)?;
32
+ let stale = existing.as_ref() != Some(&model);
33
+ let canonical_drift = write && !model_file_matches(root, &model)?;
34
+ let mut updated_paths = Vec::new();
35
+
36
+ if write && (stale || canonical_drift) {
37
+ write_model(root, &model)?;
38
+ updated_paths.push(MODEL_PATH.to_string());
39
+ }
40
+
41
+ let stale_after_write = stale && !write;
42
+ Ok(RepositoryModelRefresh {
43
+ schema: "naome.repository-model-refresh.v1".to_string(),
44
+ ok: !stale_after_write,
45
+ stale: stale_after_write,
46
+ write,
47
+ model_path: MODEL_PATH.to_string(),
48
+ updated_paths,
49
+ reason_codes: if stale {
50
+ vec!["repository_model_stale".to_string()]
51
+ } else {
52
+ Vec::new()
53
+ },
54
+ model,
55
+ })
56
+ }
57
+
58
+ pub fn explain_repository_model_path(
59
+ root: &Path,
60
+ path: impl AsRef<str>,
61
+ ) -> Result<RepositoryPathExplanation, NaomeError> {
62
+ let model = read_existing_model(root)?.unwrap_or(detect_repository_model(root)?);
63
+ Ok(explain_path(&model, path.as_ref()))
64
+ }
65
+
66
+ pub fn repository_model_drift(root: &Path) -> Result<RepositoryModelDrift, NaomeError> {
67
+ let path = root.join(MODEL_PATH);
68
+ let model_present = path.is_file();
69
+ if !model_present {
70
+ return Ok(RepositoryModelDrift {
71
+ schema: "naome.repository-model-drift.v1".to_string(),
72
+ ok: true,
73
+ model_present,
74
+ stale: false,
75
+ model_path: MODEL_PATH.to_string(),
76
+ reason_codes: Vec::new(),
77
+ related_paths: Vec::new(),
78
+ messages: Vec::new(),
79
+ });
80
+ }
81
+
82
+ let existing = read_existing_model(root)?.unwrap_or_default();
83
+ let current = detect_repository_model(root)?;
84
+ let stale = existing != current;
85
+ let related_paths = if stale {
86
+ model_evidence_paths(&current)
87
+ } else {
88
+ Vec::new()
89
+ };
90
+ let messages = if stale {
91
+ vec![format!(
92
+ "NAOME repository model is stale; run naome repo model --write to refresh deterministic repository facts in {MODEL_PATH}."
93
+ )]
94
+ } else {
95
+ Vec::new()
96
+ };
97
+
98
+ Ok(RepositoryModelDrift {
99
+ schema: "naome.repository-model-drift.v1".to_string(),
100
+ ok: !stale,
101
+ model_present,
102
+ stale,
103
+ model_path: MODEL_PATH.to_string(),
104
+ reason_codes: if stale {
105
+ vec!["repository_model_stale".to_string()]
106
+ } else {
107
+ Vec::new()
108
+ },
109
+ related_paths,
110
+ messages,
111
+ })
112
+ }
113
+
114
+ fn read_existing_model(root: &Path) -> Result<Option<RepositoryModel>, NaomeError> {
115
+ let path = root.join(MODEL_PATH);
116
+ if !path.is_file() {
117
+ return Ok(None);
118
+ }
119
+ Ok(Some(serde_json::from_str(&fs::read_to_string(path)?)?))
120
+ }
121
+
122
+ fn model_evidence_paths(model: &RepositoryModel) -> Vec<String> {
123
+ let mut paths = model
124
+ .facts
125
+ .iter()
126
+ .flat_map(|fact| fact.evidence.iter())
127
+ .chain(model.roots.iter().flat_map(|root| root.evidence.iter()))
128
+ .chain(model.entities.iter().flat_map(|entity| entity.evidence.iter()))
129
+ .chain(model.path_facts.iter().map(|fact| &fact.path))
130
+ .chain(
131
+ model
132
+ .verification_checks
133
+ .iter()
134
+ .flat_map(|check| check.evidence.iter()),
135
+ )
136
+ .filter(|path| path.as_str() != ".naome/verification.json")
137
+ .cloned()
138
+ .collect::<std::collections::BTreeSet<_>>()
139
+ .into_iter()
140
+ .collect::<Vec<_>>();
141
+ paths.sort();
142
+ paths
143
+ }
144
+
145
+ fn write_model(root: &Path, model: &RepositoryModel) -> Result<(), NaomeError> {
146
+ let path = root.join(MODEL_PATH);
147
+ if let Some(parent) = path.parent() {
148
+ fs::create_dir_all(parent)?;
149
+ }
150
+ fs::write(path, model_content(model)?)?;
151
+ Ok(())
152
+ }
153
+
154
+ fn model_file_matches(root: &Path, model: &RepositoryModel) -> Result<bool, NaomeError> {
155
+ let path = root.join(MODEL_PATH);
156
+ if !path.is_file() {
157
+ return Ok(false);
158
+ }
159
+ Ok(fs::read_to_string(path)? == model_content(model)?)
160
+ }
161
+
162
+ fn model_content(model: &RepositoryModel) -> Result<String, NaomeError> {
163
+ Ok(format!("{}\n", serde_json::to_string(model)?))
164
+ }
@@ -2,7 +2,7 @@ use std::path::Path;
2
2
 
3
3
  use crate::harness_health::{validate_harness_health, HarnessHealthOptions};
4
4
  use crate::models::NaomeError;
5
- use crate::quality::{check_repository_quality, QualityMode};
5
+ use crate::quality::{check_repository_quality, check_semantic_legacy, QualityMode};
6
6
  use crate::route::git_ops::{command_output, git_output};
7
7
  use crate::route::quality_gate::QualityCheck;
8
8
  use crate::task_state::{validate_task_state, TaskStateMode, TaskStateOptions};
@@ -64,12 +64,51 @@ pub(super) fn run_quality_check(
64
64
  )?;
65
65
  run_repository_quality_check(root)
66
66
  }
67
+ "repository-semantic-check" => {
68
+ require_builtin_quality_check_any(
69
+ check_id,
70
+ check,
71
+ &[
72
+ "naome semantic check --changed",
73
+ "node .naome/bin/naome.js semantic check --changed",
74
+ "npm run check:repository-semantic",
75
+ ],
76
+ )?;
77
+ run_repository_semantic_check(root)
78
+ }
67
79
  _ => Err(NaomeError::new(format!(
68
80
  "Quality check {check_id} is not a built-in safe check; NAOME will not execute repository-controlled verification commands."
69
81
  ))),
70
82
  }
71
83
  }
72
84
 
85
+ fn run_repository_semantic_check(root: &Path) -> Result<(), NaomeError> {
86
+ let report = check_semantic_legacy(root, QualityMode::ChangedFast)?;
87
+ if report.ok {
88
+ return Ok(());
89
+ }
90
+
91
+ let details = report
92
+ .findings
93
+ .iter()
94
+ .take(20)
95
+ .flat_map(|finding| {
96
+ finding.occurrences.iter().take(1).map(move |occurrence| {
97
+ format!(
98
+ "{}:{} semantic-{}: {}",
99
+ occurrence.path, occurrence.start_line, finding.kind, finding.summary
100
+ )
101
+ })
102
+ })
103
+ .collect::<Vec<_>>()
104
+ .join("\n");
105
+ Err(NaomeError::new(format!(
106
+ "repository-semantic-check failed with {} finding(s).\n{}",
107
+ report.findings.len(),
108
+ details
109
+ )))
110
+ }
111
+
73
112
  fn run_repository_quality_check(root: &Path) -> Result<(), NaomeError> {
74
113
  let report = check_repository_quality(root, QualityMode::ChangedFast)?;
75
114
  if report.ok {
@@ -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
+ }