@lamentis/naome 1.3.17 → 1.4.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.
- package/Cargo.lock +2 -2
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/architecture_commands.rs +16 -3
- package/crates/naome-cli/src/main.rs +10 -1
- package/crates/naome-cli/src/task_commands/common.rs +32 -0
- package/crates/naome-cli/src/task_commands/readiness.rs +40 -0
- package/crates/naome-cli/src/task_commands/record.rs +134 -0
- package/crates/naome-cli/src/task_commands/repair.rs +30 -0
- package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
- package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
- package/crates/naome-cli/src/task_commands.rs +69 -1
- package/crates/naome-cli/tests/task_cli.rs +58 -0
- package/crates/naome-cli/tests/task_cli_agent_controls.rs +217 -0
- package/crates/naome-cli/tests/task_cli_control.rs +126 -0
- package/crates/naome-cli/tests/task_cli_support/mod.rs +150 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/output.rs +196 -0
- package/crates/naome-core/src/architecture/scan/cache.rs +1 -1
- package/crates/naome-core/src/architecture.rs +1 -0
- package/crates/naome-core/src/lib.rs +15 -9
- package/crates/naome-core/src/task_state/mod.rs +10 -0
- package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
- package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
- package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
- package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
- package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
- package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
- package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
- package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
- package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
- package/crates/naome-core/src/task_state/status/control.rs +16 -0
- package/crates/naome-core/src/task_state/status/git.rs +133 -0
- package/crates/naome-core/src/task_state/status/model.rs +150 -0
- package/crates/naome-core/src/task_state/status/proof.rs +167 -0
- package/crates/naome-core/src/task_state/status/proof_read.rs +150 -0
- package/crates/naome-core/src/task_state/status/report.rs +148 -0
- package/crates/naome-core/src/task_state/status/report_context.rs +126 -0
- package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
- package/crates/naome-core/src/task_state/status/scope.rs +111 -0
- package/crates/naome-core/src/task_state/status/transition.rs +73 -0
- package/crates/naome-core/src/task_state/status.rs +23 -0
- package/crates/naome-core/src/task_state/status_output.rs +103 -0
- package/crates/naome-core/tests/architecture_cache.rs +1 -1
- package/crates/naome-core/tests/architecture_config.rs +68 -1
- package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
- package/crates/naome-core/tests/task_state_support/states.rs +4 -0
- package/crates/naome-core/tests/task_status.rs +301 -0
- package/crates/naome-core/tests/task_status_git.rs +141 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +1 -1
- package/templates/naome-root/.naome/bin/check-task-state.js +1 -1
- package/templates/naome-root/.naome/manifest.json +2 -2
- package/templates/naome-root/docs/naome/architecture-fitness.md +23 -23
package/Cargo.lock
CHANGED
|
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|
|
76
76
|
|
|
77
77
|
[[package]]
|
|
78
78
|
name = "naome-cli"
|
|
79
|
-
version = "1.
|
|
79
|
+
version = "1.4.1"
|
|
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.
|
|
87
|
+
version = "1.4.1"
|
|
88
88
|
dependencies = [
|
|
89
89
|
"serde",
|
|
90
90
|
"serde_json",
|
|
@@ -2,9 +2,9 @@ use std::fs;
|
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
3
|
|
|
4
4
|
use naome_core::{
|
|
5
|
-
config_findings_for, format_architecture_explain,
|
|
6
|
-
format_architecture_validation, scan_architecture,
|
|
7
|
-
ArchitectureScanOptions, ARCHITECTURE_RULE_IDS,
|
|
5
|
+
architecture_validation_sarif_with_root, config_findings_for, format_architecture_explain,
|
|
6
|
+
format_architecture_scan, format_architecture_validation, scan_architecture,
|
|
7
|
+
validate_architecture, ArchitectureScanOptions, ARCHITECTURE_RULE_IDS,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
use crate::architecture_init::architecture_init_config_text;
|
|
@@ -88,6 +88,19 @@ fn run_arch_validate(root: &Path, args: &[String]) -> Result<(), Box<dyn std::er
|
|
|
88
88
|
root,
|
|
89
89
|
scan_options(root, args, has_flag(args, "--changed-only")),
|
|
90
90
|
)?;
|
|
91
|
+
if has_flag(args, "--sarif") {
|
|
92
|
+
let output =
|
|
93
|
+
serde_json::to_string_pretty(&architecture_validation_sarif_with_root(&report, root))?;
|
|
94
|
+
if let Some(path) = option_value(args, "--output") {
|
|
95
|
+
fs::write(root.join(path), output)?;
|
|
96
|
+
} else {
|
|
97
|
+
println!("{output}");
|
|
98
|
+
}
|
|
99
|
+
if report.status == "fail" {
|
|
100
|
+
std::process::exit(1);
|
|
101
|
+
}
|
|
102
|
+
return Ok(());
|
|
103
|
+
}
|
|
91
104
|
let json = has_flag(args, "--json") || has_flag(args, "--agent-feedback");
|
|
92
105
|
if json {
|
|
93
106
|
if has_flag(args, "--agent-feedback") {
|
|
@@ -37,6 +37,15 @@ const HELP: &str = r#"Usage:
|
|
|
37
37
|
naome sync [--package-root <path>] [--installer-js <path>]
|
|
38
38
|
naome install-plan [--harness-version <version>]
|
|
39
39
|
naome seed-verification
|
|
40
|
+
naome task status [--json] [--exit-code]
|
|
41
|
+
naome task proof-plan [--json] [--exit-code]
|
|
42
|
+
naome task can-transition --to complete [--json]
|
|
43
|
+
naome task can-commit --json
|
|
44
|
+
naome task repair --plan <id> --dry-run --json
|
|
45
|
+
naome task record-proof --from-proof-plan [--dry-run] --json
|
|
46
|
+
naome task request-scope --path <path> --reason <reason> --json
|
|
47
|
+
naome task timeline --json
|
|
48
|
+
naome task loop-snapshot --json
|
|
40
49
|
naome task render-state [--write] [--json]
|
|
41
50
|
naome task migrate-ledger [--write] [--json]
|
|
42
51
|
naome refresh-integrity [--root <path>] [--json]
|
|
@@ -62,7 +71,7 @@ const HELP: &str = r#"Usage:
|
|
|
62
71
|
naome arch init [--config <path>] [--json]
|
|
63
72
|
naome arch explain [--config <path>] [--json]
|
|
64
73
|
naome arch scan [--config <path>] [--changed-only] [--write] [--output <path>] [--json]
|
|
65
|
-
naome arch validate [--config <path>] [--changed-only] [--agent-feedback] [--json]
|
|
74
|
+
naome arch validate [--config <path>] [--changed-only] [--agent-feedback] [--json|--sarif] [--output <path>]
|
|
66
75
|
naome workflow search-profile [--json]
|
|
67
76
|
naome workflow agent-plan [--json]
|
|
68
77
|
naome workflow context-delta [--read-path <path>...] [--json]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use serde_json::Value;
|
|
5
|
+
|
|
6
|
+
pub(super) fn read_task_state(root: &Path) -> Result<Value, Box<dyn std::error::Error>> {
|
|
7
|
+
Ok(serde_json::from_str(&fs::read_to_string(
|
|
8
|
+
root.join(".naome/task-state.json"),
|
|
9
|
+
)?)?)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
pub(super) fn print_json(value: Value) -> Result<(), Box<dyn std::error::Error>> {
|
|
13
|
+
println!("{}", serde_json::to_string_pretty(&value)?);
|
|
14
|
+
Ok(())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub(super) fn value_after<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
|
|
18
|
+
args.windows(2)
|
|
19
|
+
.find(|window| window[0] == flag)
|
|
20
|
+
.map(|window| window[1].as_str())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub(super) fn repeated_values(args: &[String], flag: &str) -> Vec<String> {
|
|
24
|
+
let mut values = args
|
|
25
|
+
.windows(2)
|
|
26
|
+
.filter(|window| window[0] == flag)
|
|
27
|
+
.map(|window| window[1].clone())
|
|
28
|
+
.collect::<Vec<_>>();
|
|
29
|
+
values.sort();
|
|
30
|
+
values.dedup();
|
|
31
|
+
values
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use naome_core::{task_status_report, task_transition_readiness};
|
|
5
|
+
use serde_json::json;
|
|
6
|
+
|
|
7
|
+
use super::common::print_json;
|
|
8
|
+
|
|
9
|
+
pub(super) fn can_commit(root: &Path, _args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
10
|
+
let status = task_status_report(root)?;
|
|
11
|
+
let transition = task_transition_readiness(root, "complete")?;
|
|
12
|
+
let required = commit_requirements(&status, &transition.blocking_findings);
|
|
13
|
+
print_json(json!({
|
|
14
|
+
"schema": "naome.task.commit-readiness.v1",
|
|
15
|
+
"allowed": transition.allowed && status.agent_loop.can_commit && required.is_empty(),
|
|
16
|
+
"commitPaths": status.scope.in_scope_changed_paths,
|
|
17
|
+
"blockingFindings": transition.blocking_findings,
|
|
18
|
+
"requiredBeforeCommit": required,
|
|
19
|
+
"agentLoop": status.agent_loop
|
|
20
|
+
}))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn commit_requirements(
|
|
24
|
+
status: &naome_core::TaskStatusReportV1,
|
|
25
|
+
blockers: &[naome_core::TaskStatusFinding],
|
|
26
|
+
) -> Vec<String> {
|
|
27
|
+
let mut requirements = BTreeSet::new();
|
|
28
|
+
for check in status
|
|
29
|
+
.proof
|
|
30
|
+
.missing_checks
|
|
31
|
+
.iter()
|
|
32
|
+
.chain(status.proof.stale_checks.iter())
|
|
33
|
+
{
|
|
34
|
+
requirements.insert(format!("Run and record required check: {check}."));
|
|
35
|
+
}
|
|
36
|
+
for finding in blockers {
|
|
37
|
+
requirements.insert(finding.suggested_fix.clone());
|
|
38
|
+
}
|
|
39
|
+
requirements.into_iter().collect()
|
|
40
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use naome_core::task_proof_plan;
|
|
5
|
+
use serde_json::{json, Value};
|
|
6
|
+
|
|
7
|
+
use super::common::{print_json, read_task_state};
|
|
8
|
+
|
|
9
|
+
pub(super) fn record_proof(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
10
|
+
if !args.iter().any(|arg| arg == "--from-proof-plan") {
|
|
11
|
+
return Err("naome task record-proof requires --from-proof-plan".into());
|
|
12
|
+
}
|
|
13
|
+
let plan = task_proof_plan(root)?;
|
|
14
|
+
let dry_run = args.iter().any(|arg| arg == "--dry-run");
|
|
15
|
+
let recorded = !dry_run && !plan.proof_recording.checks_to_record.is_empty();
|
|
16
|
+
let recorded_ids = if recorded {
|
|
17
|
+
write_proof_batch(root, &plan.proof_recording)?
|
|
18
|
+
} else {
|
|
19
|
+
RecordedProof {
|
|
20
|
+
path_set_id: plan.proof_recording.path_set_id.clone(),
|
|
21
|
+
proof_batch_id: plan.proof_recording.proof_batch_id.clone(),
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
if dry_run && !plan.proof_recording.checks_to_record.is_empty() {
|
|
25
|
+
print_json(json!({
|
|
26
|
+
"schema": "naome.task.record-proof.v1",
|
|
27
|
+
"dryRun": dry_run,
|
|
28
|
+
"recorded": recorded,
|
|
29
|
+
"pathSetId": recorded_ids.path_set_id,
|
|
30
|
+
"paths": plan.proof_recording.paths,
|
|
31
|
+
"proofBatchId": recorded_ids.proof_batch_id,
|
|
32
|
+
"checksRecorded": plan.proof_recording.checks_to_record,
|
|
33
|
+
"agentInstruction": "Record proof only after the listed checks exited 0 for the current task diff."
|
|
34
|
+
}))?;
|
|
35
|
+
return Ok(());
|
|
36
|
+
}
|
|
37
|
+
print_json(json!({
|
|
38
|
+
"schema": "naome.task.record-proof.v1",
|
|
39
|
+
"dryRun": dry_run,
|
|
40
|
+
"recorded": recorded,
|
|
41
|
+
"pathSetId": recorded_ids.path_set_id,
|
|
42
|
+
"paths": plan.proof_recording.paths,
|
|
43
|
+
"proofBatchId": recorded_ids.proof_batch_id,
|
|
44
|
+
"checksRecorded": plan.proof_recording.checks_to_record,
|
|
45
|
+
"agentInstruction": "Record proof only after the listed checks exited 0 for the current task diff."
|
|
46
|
+
}))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
struct RecordedProof {
|
|
50
|
+
path_set_id: String,
|
|
51
|
+
proof_batch_id: String,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn write_proof_batch(
|
|
55
|
+
root: &Path,
|
|
56
|
+
recording: &naome_core::ProofRecording,
|
|
57
|
+
) -> Result<RecordedProof, Box<dyn std::error::Error>> {
|
|
58
|
+
let path = root.join(".naome/task-state.json");
|
|
59
|
+
let mut state = read_task_state(root)?;
|
|
60
|
+
let checked_at = state
|
|
61
|
+
.get("updatedAt")
|
|
62
|
+
.and_then(Value::as_str)
|
|
63
|
+
.unwrap_or("1970-01-01T00:00:00.000Z")
|
|
64
|
+
.to_string();
|
|
65
|
+
let active = state
|
|
66
|
+
.get_mut("activeTask")
|
|
67
|
+
.and_then(Value::as_object_mut)
|
|
68
|
+
.ok_or("task-state has no activeTask object")?;
|
|
69
|
+
let path_sets = active
|
|
70
|
+
.entry("proofPathSets")
|
|
71
|
+
.or_insert_with(|| json!({}))
|
|
72
|
+
.as_object_mut()
|
|
73
|
+
.ok_or("activeTask.proofPathSets must be an object")?;
|
|
74
|
+
let path_set_id = unique_path_set_id(path_sets, &recording.path_set_id);
|
|
75
|
+
path_sets.insert(path_set_id.clone(), json!(recording.paths));
|
|
76
|
+
let batches = active
|
|
77
|
+
.entry("proofBatches")
|
|
78
|
+
.or_insert_with(|| json!([]))
|
|
79
|
+
.as_array_mut()
|
|
80
|
+
.ok_or("activeTask.proofBatches must be an array")?;
|
|
81
|
+
let proof_batch_id = unique_proof_batch_id(batches, &recording.proof_batch_id);
|
|
82
|
+
batches.push(proof_batch(
|
|
83
|
+
recording,
|
|
84
|
+
&checked_at,
|
|
85
|
+
&path_set_id,
|
|
86
|
+
&proof_batch_id,
|
|
87
|
+
));
|
|
88
|
+
fs::write(path, format!("{}\n", serde_json::to_string_pretty(&state)?))?;
|
|
89
|
+
Ok(RecordedProof {
|
|
90
|
+
path_set_id,
|
|
91
|
+
proof_batch_id,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn proof_batch(
|
|
96
|
+
recording: &naome_core::ProofRecording,
|
|
97
|
+
checked_at: &str,
|
|
98
|
+
path_set_id: &str,
|
|
99
|
+
proof_batch_id: &str,
|
|
100
|
+
) -> Value {
|
|
101
|
+
json!({
|
|
102
|
+
"id": proof_batch_id,
|
|
103
|
+
"checkedAt": checked_at,
|
|
104
|
+
"evidencePathSet": path_set_id,
|
|
105
|
+
"proofs": recording.checks_to_record.iter().map(|check_id| {
|
|
106
|
+
json!({ "checkId": check_id, "exitCode": 0 })
|
|
107
|
+
}).collect::<Vec<_>>()
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn unique_path_set_id(path_sets: &serde_json::Map<String, Value>, base: &str) -> String {
|
|
112
|
+
unique_id(base, |candidate| path_sets.contains_key(candidate))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn unique_proof_batch_id(batches: &[Value], base: &str) -> String {
|
|
116
|
+
unique_id(base, |candidate| {
|
|
117
|
+
batches
|
|
118
|
+
.iter()
|
|
119
|
+
.any(|batch| batch.get("id").and_then(Value::as_str) == Some(candidate))
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn unique_id(base: &str, exists: impl Fn(&str) -> bool) -> String {
|
|
124
|
+
if !exists(base) {
|
|
125
|
+
return base.to_string();
|
|
126
|
+
}
|
|
127
|
+
for index in 2.. {
|
|
128
|
+
let candidate = format!("{base}-{index}");
|
|
129
|
+
if !exists(&candidate) {
|
|
130
|
+
return candidate;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
unreachable!("unbounded proof id suffix search must find an available id")
|
|
134
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use naome_core::task_status_report;
|
|
4
|
+
use serde_json::json;
|
|
5
|
+
|
|
6
|
+
use super::common::{print_json, value_after};
|
|
7
|
+
|
|
8
|
+
pub(super) fn repair_preview(
|
|
9
|
+
root: &Path,
|
|
10
|
+
args: &[String],
|
|
11
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
12
|
+
if !args.iter().any(|arg| arg == "--dry-run") {
|
|
13
|
+
return Err("naome task repair requires --dry-run in v1.4.1".into());
|
|
14
|
+
}
|
|
15
|
+
let plan_id = value_after(args, "--plan").ok_or("naome task repair requires --plan <id>")?;
|
|
16
|
+
let status = task_status_report(root)?;
|
|
17
|
+
let plan = status
|
|
18
|
+
.repair_plan
|
|
19
|
+
.iter()
|
|
20
|
+
.find(|item| item.id == plan_id)
|
|
21
|
+
.cloned();
|
|
22
|
+
print_json(json!({
|
|
23
|
+
"schema": "naome.task.repair-preview.v1",
|
|
24
|
+
"planId": plan_id,
|
|
25
|
+
"found": plan.is_some(),
|
|
26
|
+
"wouldExecute": false,
|
|
27
|
+
"plan": plan,
|
|
28
|
+
"agentInstruction": "Review this dry-run output and execute only safe commands explicitly allowed by NAOME."
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::json;
|
|
4
|
+
|
|
5
|
+
use super::common::{print_json, repeated_values, value_after};
|
|
6
|
+
|
|
7
|
+
pub(super) fn request_scope(
|
|
8
|
+
_root: &Path,
|
|
9
|
+
args: &[String],
|
|
10
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
11
|
+
let requested_paths = repeated_values(args, "--path");
|
|
12
|
+
if requested_paths.is_empty() {
|
|
13
|
+
return Err("naome task request-scope requires at least one --path".into());
|
|
14
|
+
}
|
|
15
|
+
let reason = value_after(args, "--reason").unwrap_or("No reason provided.");
|
|
16
|
+
print_json(json!({
|
|
17
|
+
"schema": "naome.task.scope-request.v1",
|
|
18
|
+
"wouldMutate": false,
|
|
19
|
+
"requestedPaths": requested_paths,
|
|
20
|
+
"reason": reason,
|
|
21
|
+
"suggestedFix": "Ask for an explicit task-scope revision before editing these paths.",
|
|
22
|
+
"agentInstruction": "Do not edit requested paths until a human or deterministic task revision admits them."
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use naome_core::task_status_report;
|
|
4
|
+
use serde_json::{json, Value};
|
|
5
|
+
|
|
6
|
+
use super::common::{print_json, read_task_state};
|
|
7
|
+
|
|
8
|
+
pub(super) fn timeline(root: &Path, _args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
9
|
+
let status = task_status_report(root)?;
|
|
10
|
+
let state = read_task_state(root)?;
|
|
11
|
+
let active = state.get("activeTask").unwrap_or(&Value::Null);
|
|
12
|
+
let mut events = admission_events(active);
|
|
13
|
+
events.extend(proof_events(active));
|
|
14
|
+
events.push(json!({ "type": "current_diff", "paths": status.scope.changed_paths }));
|
|
15
|
+
print_json(json!({
|
|
16
|
+
"schema": "naome.task.timeline.v1",
|
|
17
|
+
"state": status.state,
|
|
18
|
+
"taskId": status.task_id,
|
|
19
|
+
"events": events
|
|
20
|
+
}))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub(super) fn loop_snapshot(
|
|
24
|
+
root: &Path,
|
|
25
|
+
_args: &[String],
|
|
26
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
27
|
+
let status = task_status_report(root)?;
|
|
28
|
+
print_json(json!({
|
|
29
|
+
"schema": "naome.task.loop-snapshot.v1",
|
|
30
|
+
"taskId": status.task_id,
|
|
31
|
+
"state": status.state,
|
|
32
|
+
"status": {
|
|
33
|
+
"agentLoop": status.agent_loop,
|
|
34
|
+
"nextActionV2": status.next_action_v2,
|
|
35
|
+
"findings": status.findings,
|
|
36
|
+
"repairPlan": status.repair_plan,
|
|
37
|
+
"policyHints": status.policy_hints,
|
|
38
|
+
"recoveryGuidance": status.recovery_guidance
|
|
39
|
+
},
|
|
40
|
+
"proof": status.proof
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn admission_events(active: &Value) -> Vec<Value> {
|
|
45
|
+
active
|
|
46
|
+
.get("admission")
|
|
47
|
+
.map(|admission| {
|
|
48
|
+
vec![json!({
|
|
49
|
+
"type": "admission",
|
|
50
|
+
"gitHead": admission.get("gitHead").cloned().unwrap_or(Value::Null),
|
|
51
|
+
"checkedAt": admission.get("checkedAt").cloned().unwrap_or(Value::Null)
|
|
52
|
+
})]
|
|
53
|
+
})
|
|
54
|
+
.unwrap_or_default()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn proof_events(active: &Value) -> Vec<Value> {
|
|
58
|
+
active
|
|
59
|
+
.get("proofBatches")
|
|
60
|
+
.and_then(Value::as_array)
|
|
61
|
+
.into_iter()
|
|
62
|
+
.flatten()
|
|
63
|
+
.map(|batch| {
|
|
64
|
+
json!({
|
|
65
|
+
"type": "proof_batch",
|
|
66
|
+
"id": batch.get("id").cloned().unwrap_or(Value::Null),
|
|
67
|
+
"checkedAt": batch.get("checkedAt").cloned().unwrap_or(Value::Null)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
.collect()
|
|
71
|
+
}
|
|
@@ -1,15 +1,83 @@
|
|
|
1
1
|
use std::path::Path;
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
mod common;
|
|
4
|
+
mod readiness;
|
|
5
|
+
mod record;
|
|
6
|
+
mod repair;
|
|
7
|
+
mod scope_request;
|
|
8
|
+
mod timeline;
|
|
9
|
+
|
|
10
|
+
use naome_core::{
|
|
11
|
+
format_task_proof_plan, format_task_status, migrate_task_state_to_ledger,
|
|
12
|
+
render_task_state_from_ledger, task_proof_plan, task_status_exit_code, task_status_report,
|
|
13
|
+
task_transition_readiness,
|
|
14
|
+
};
|
|
4
15
|
|
|
5
16
|
pub fn run_task_command(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
6
17
|
match args.get(1).map(String::as_str) {
|
|
7
18
|
Some("render-state") => render_state(root, args),
|
|
8
19
|
Some("migrate-ledger") => migrate_ledger(root, args),
|
|
20
|
+
Some("status") => task_status(root, args),
|
|
21
|
+
Some("proof-plan") => proof_plan(root, args),
|
|
22
|
+
Some("can-transition") => can_transition(root, args),
|
|
23
|
+
Some("repair") => repair::repair_preview(root, args),
|
|
24
|
+
Some("record-proof") => record::record_proof(root, args),
|
|
25
|
+
Some("request-scope") => scope_request::request_scope(root, args),
|
|
26
|
+
Some("can-commit") => readiness::can_commit(root, args),
|
|
27
|
+
Some("timeline") => timeline::timeline(root, args),
|
|
28
|
+
Some("loop-snapshot") => timeline::loop_snapshot(root, args),
|
|
9
29
|
_ => Err("unknown task command".into()),
|
|
10
30
|
}
|
|
11
31
|
}
|
|
12
32
|
|
|
33
|
+
fn task_status(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
34
|
+
let report = task_status_report(root)?;
|
|
35
|
+
if args.iter().any(|arg| arg == "--json") {
|
|
36
|
+
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
37
|
+
} else {
|
|
38
|
+
print!("{}", format_task_status(&report));
|
|
39
|
+
}
|
|
40
|
+
exit_if_requested(args, task_status_exit_code(&report.findings, &report.proof));
|
|
41
|
+
Ok(())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn proof_plan(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
45
|
+
let report = task_proof_plan(root)?;
|
|
46
|
+
if args.iter().any(|arg| arg == "--json") {
|
|
47
|
+
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
48
|
+
} else {
|
|
49
|
+
print!("{}", format_task_proof_plan(&report));
|
|
50
|
+
}
|
|
51
|
+
exit_if_requested(args, task_status_exit_code(&report.findings, &report.proof));
|
|
52
|
+
Ok(())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn can_transition(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
56
|
+
let Some(target) = args
|
|
57
|
+
.windows(2)
|
|
58
|
+
.find(|window| window[0] == "--to")
|
|
59
|
+
.map(|window| window[1].as_str())
|
|
60
|
+
else {
|
|
61
|
+
return Err("naome task can-transition requires --to complete".into());
|
|
62
|
+
};
|
|
63
|
+
let report = task_transition_readiness(root, target)?;
|
|
64
|
+
if args.iter().any(|arg| arg == "--json") {
|
|
65
|
+
println!("{}", serde_json::to_string_pretty(&report)?);
|
|
66
|
+
} else {
|
|
67
|
+
println!(
|
|
68
|
+
"NAOME task transition {target}: {}",
|
|
69
|
+
if report.allowed { "allowed" } else { "blocked" }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
Ok(())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn exit_if_requested(args: &[String], code: i32) {
|
|
76
|
+
if args.iter().any(|arg| arg == "--exit-code") {
|
|
77
|
+
std::process::exit(code);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
13
81
|
fn render_state(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
|
14
82
|
let write = args.iter().any(|arg| arg == "--write");
|
|
15
83
|
let json = args.iter().any(|arg| arg == "--json");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
use std::process::Command;
|
|
2
|
+
|
|
3
|
+
use serde_json::{json, Value};
|
|
4
|
+
|
|
5
|
+
mod task_cli_support;
|
|
6
|
+
|
|
7
|
+
use task_cli_support::{fixture_root, init_git, task_state, write_fixture_file};
|
|
8
|
+
|
|
9
|
+
#[test]
|
|
10
|
+
fn task_status_json_reports_scope_and_next_action() {
|
|
11
|
+
let root = fixture_root(task_state());
|
|
12
|
+
init_git(&root);
|
|
13
|
+
write_fixture_file(&root, "README.md", "changed\n");
|
|
14
|
+
|
|
15
|
+
let output = Command::new(env!("CARGO_BIN_EXE_naome"))
|
|
16
|
+
.args(["task", "status", "--json"])
|
|
17
|
+
.current_dir(&root)
|
|
18
|
+
.output()
|
|
19
|
+
.unwrap();
|
|
20
|
+
|
|
21
|
+
assert_success(&output);
|
|
22
|
+
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
23
|
+
assert_eq!(payload["schema"], "naome.task.status.v1");
|
|
24
|
+
assert_eq!(
|
|
25
|
+
payload["scope"]["inScopeChangedPaths"],
|
|
26
|
+
json!(["README.md"])
|
|
27
|
+
);
|
|
28
|
+
assert_eq!(payload["nextActionV2"]["type"], "rerun_checks");
|
|
29
|
+
assert_eq!(payload["agentLoop"]["state"], "blocked_by_missing_proof");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[test]
|
|
33
|
+
fn task_proof_plan_human_output_lists_recommended_commands() {
|
|
34
|
+
let root = fixture_root(task_state());
|
|
35
|
+
init_git(&root);
|
|
36
|
+
write_fixture_file(&root, "README.md", "changed\n");
|
|
37
|
+
|
|
38
|
+
let output = Command::new(env!("CARGO_BIN_EXE_naome"))
|
|
39
|
+
.args(["task", "proof-plan"])
|
|
40
|
+
.current_dir(root)
|
|
41
|
+
.output()
|
|
42
|
+
.unwrap();
|
|
43
|
+
|
|
44
|
+
assert_success(&output);
|
|
45
|
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
46
|
+
assert!(stdout.contains("agent loop: blocked_by_missing_proof"));
|
|
47
|
+
assert!(stdout.contains("recommended commands:"));
|
|
48
|
+
assert!(stdout.contains("diff-check: git diff --check"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn assert_success(output: &std::process::Output) {
|
|
52
|
+
assert!(
|
|
53
|
+
output.status.success(),
|
|
54
|
+
"{}{}",
|
|
55
|
+
String::from_utf8_lossy(&output.stdout),
|
|
56
|
+
String::from_utf8_lossy(&output.stderr)
|
|
57
|
+
);
|
|
58
|
+
}
|