Jesús Pérez da083fb9ec
Some checks failed
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
.coder/m
2026-03-29 00:19:56 +00:00

391 lines
15 KiB
Plaintext

#!/usr/bin/env nu
# reflection/modules/run.nu — step execution tracking and run lifecycle.
# Provides: run start, step report, run status
#
# Storage: .coder/<actor>/runs/<run_id>/
# run.json — run metadata
# steps.jsonl — one JSON record per reported step
# current.json — pointer to active run (in .coder/<actor>/runs/)
use ../modules/store.nu [daemon-export-safe]
use ../modules/describe.nu [nickel-import-path]
def project-root []: nothing -> string {
let pr = ($env.ONTOREF_PROJECT_ROOT? | default "")
if ($pr | is-not-empty) and ($pr != $env.ONTOREF_ROOT) { $pr } else { $env.ONTOREF_ROOT }
}
def runs-dir []: nothing -> string {
let root = (project-root)
let actor = ($env.ONTOREF_ACTOR? | default "developer")
$"($root)/.coder/($actor)/runs"
}
def current-run-file []: nothing -> string {
$"(runs-dir)/current.json"
}
def load-current-run []: nothing -> record {
let f = (current-run-file)
if not ($f | path exists) { return {} }
open $f
}
def load-run-steps [run_id: string]: nothing -> list<record> {
let f = $"(runs-dir)/($run_id)/steps.jsonl"
if not ($f | path exists) { return [] }
open $f | lines | where { |l| $l | str trim | is-not-empty } | each { |l| $l | from json }
}
def find-mode-file [mode: string]: nothing -> string {
let root = (project-root)
let project_file = $"($root)/reflection/modes/($mode).ncl"
let ontoref_file = $"($env.ONTOREF_ROOT)/reflection/modes/($mode).ncl"
if ($project_file | path exists) { $project_file } else if ($ontoref_file | path exists) { $ontoref_file } else { "" }
}
def load-mode-dag [mode: string]: nothing -> record {
let root = (project-root)
let mode_file = (find-mode-file $mode)
if ($mode_file | is-empty) {
error make { msg: $"Mode '($mode)' not found in project or ontoref reflection/modes/" }
}
let mode_root = if ($mode_file | str starts-with $root) and ($root != $env.ONTOREF_ROOT) {
$root
} else {
$env.ONTOREF_ROOT
}
let ip = (nickel-import-path $mode_root)
let result = (daemon-export-safe $mode_file --import-path $ip)
if $result == null {
error make { msg: $"Failed to export mode '($mode)' — check NCL syntax and import paths" }
}
$result
}
def now-iso []: nothing -> string {
date now | format date "%Y-%m-%dT%H:%M:%SZ"
}
def run-id-for [mode: string, task: string]: nothing -> string {
if ($task | is-not-empty) {
$"($mode)-($task)"
} else {
let ts = (date now | format date "%Y%m%dT%H%M%S")
$"($mode)-($ts)"
}
}
# Start a new run for a mode. Establishes run context for subsequent `step report` calls.
export def "run start" [
mode: string, # Mode ID (must exist in reflection/modes/)
--task (-t): string = "", # Backlog task ID to associate with this run
--fmt (-f): string = "",
]: nothing -> nothing {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
let mode_data = (load-mode-dag $mode)
let run_id = (run-id-for $mode $task)
let dir = $"(runs-dir)/($run_id)"
mkdir $dir
let step_count = ($mode_data.steps? | default [] | length)
let state = {
run_id: $run_id,
mode: $mode,
task: $task,
actor: $actor,
started_at: (now-iso),
steps_total: $step_count,
}
$state | to json | save --force $"($dir)/run.json"
$state | to json | save --force (current-run-file)
# Seed empty steps.jsonl so append works consistently
"" | save --force $"($dir)/steps.jsonl"
if $fmt == "json" {
print ($state | to json)
} else {
print $"Run started: ($run_id)"
print $" Mode: ($mode) ($step_count) steps"
if ($task | is-not-empty) { print $" Task: ($task)" }
print $" Dir: ($dir)"
print $" Next: ontoref step report ($mode) <step_id> --status pass|fail"
}
}
# Report the result of a step. Validates step exists and dependencies are satisfied.
export def "step report" [
mode: string, # Mode ID
step_id: string, # Step ID to report
--status (-s): string, # pass | fail | skip
--exit-code (-e): int = 0,
--artifacts (-a): list<string> = [],
--warnings (-w): int = 0,
--run (-r): string = "", # Override run ID (default: active run)
--fmt (-f): string = "",
]: nothing -> nothing {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
if not ($status in ["pass", "fail", "skip"]) {
error make { msg: $"Invalid status '($status)'. Must be: pass | fail | skip" }
}
# Resolve run context
let current = (load-current-run)
let run_id = if ($run | is-not-empty) {
$run
} else if ($current | is-not-empty) and ($current.mode? | default "") == $mode {
$current.run_id? | default ""
} else {
error make { msg: $"No active run for mode '($mode)'. Start one with: ontoref run start ($mode)" }
}
let dir = $"(runs-dir)/($run_id)"
if not ($dir | path exists) {
error make { msg: $"Run directory not found: ($dir). Start with: ontoref run start ($mode)" }
}
let steps_file = $"($dir)/steps.jsonl"
# Validate step exists in mode DAG
let mode_data = (load-mode-dag $mode)
let matching_steps = ($mode_data.steps? | default [] | where { |s| $s.id == $step_id })
let step_def = if ($matching_steps | is-not-empty) { $matching_steps | first } else { null }
if ($step_def | is-empty) {
error make { msg: $"Step '($step_id)' not found in mode '($mode)'. Valid steps: ($mode_data.steps? | default [] | each { |s| $s.id } | str join ', ')" }
}
# Validate blocking dependencies are satisfied
let completed = (load-run-steps $run_id)
let blocking = ($step_def.depends_on? | default [] | where { |d|
($d.kind? | default "Always") in ["Always", "OnSuccess"]
})
for dep in $blocking {
let prev_list = ($completed | where { |r| $r.step == $dep.step })
let prev = if ($prev_list | is-not-empty) { $prev_list | first } else { null }
if ($prev | is-empty) {
error make { msg: $"Step '($step_id)' requires '($dep.step)' to be reported first" }
}
if ($dep.kind? | default "Always") == "OnSuccess" and ($prev.status? | default "") != "pass" {
error make { msg: $"Step '($step_id)' depends on '($dep.step)' with OnSuccess, but it ($prev.status? | default 'unknown')" }
}
}
let entry = {
run_id: $run_id,
mode: $mode,
step: $step_id,
status: $status,
exit_code: $exit_code,
warnings: $warnings,
artifacts: $artifacts,
actor: $actor,
ts: (now-iso),
}
$"\n($entry | to json -r)" | save --append $steps_file
# Emit side-effect on fail+Stop
let on_error_strategy = ($step_def.on_error?.strategy? | default "Stop")
if $status == "fail" and $on_error_strategy == "Stop" {
if $fmt != "json" {
print $"(ansi red)BLOCKED(ansi reset): step '($step_id)' failed with on_error=Stop"
print $" Run '($run_id)' is now blocked. Resolve and re-run this step."
}
}
if $fmt == "json" {
print ($entry | to json)
} else {
let mark = if $status == "pass" { $"(ansi green)✓(ansi reset)" } else if $status == "fail" { $"(ansi red)✗(ansi reset)" } else { $"(ansi yellow)-(ansi reset)" }
print $" ($mark) ($step_id) [($mode) / ($run_id)]"
if $warnings > 0 { print $" ($warnings) warning(s)" }
if ($artifacts | is-not-empty) { print $" artifacts: ($artifacts | str join ', ')" }
}
}
# Show current run progress.
export def "run status" [
--run (-r): string = "", # Specific run ID (default: active run)
--fmt (-f): string = "",
]: nothing -> nothing {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
let current = (load-current-run)
let run_id = if ($run | is-not-empty) { $run } else if ($current | is-not-empty) { $current.run_id? | default "" } else { "" }
if ($run_id | is-empty) {
if $fmt == "json" { print "null" } else { print "No active run. Start with: ontoref run start <mode>" }
return
}
let mode_id = ($current.mode? | default "")
let mode_data = if ($mode_id | is-not-empty) {
try { load-mode-dag $mode_id } catch { {} }
} else { {} }
let all_step_ids = ($mode_data.steps? | default [] | each { |s| $s.id })
let reported = (load-run-steps $run_id)
let steps_view = ($all_step_ids | each { |id|
let r_list = ($reported | where { |s| $s.step == $id })
let r = if ($r_list | is-not-empty) { $r_list | first } else { null }
if ($r | is-not-empty) {
{ id: $id, status: $r.status, exit_code: ($r.exit_code? | default 0), ts: ($r.ts? | default ""), warnings: ($r.warnings? | default 0) }
} else {
{ id: $id, status: "pending", exit_code: null, ts: "", warnings: 0 }
}
})
let data = {
run_id: $run_id,
mode: $mode_id,
task: ($current.task? | default ""),
started_at: ($current.started_at? | default ""),
steps_total: ($all_step_ids | length),
steps_reported: ($reported | length),
steps: $steps_view,
}
if $fmt == "json" {
print ($data | to json)
} else {
print ""
print $"RUN: ($data.run_id)"
print "══════════════════════════════════════════════════════════════════"
if ($data.task | is-not-empty) { print $" Task: ($data.task)" }
print $" Mode: ($data.mode)"
print $" Started: ($data.started_at)"
print $" Progress: ($data.steps_reported)/($data.steps_total) steps reported"
print ""
for s in $data.steps {
let mark = match $s.status {
"pass" => $"(ansi green)✓(ansi reset)",
"fail" => $"(ansi red)✗(ansi reset)",
"skip" => $"(ansi yellow)-(ansi reset)",
"pending" => $"(ansi dark_gray)·(ansi reset)",
_ => " ",
}
let warn_tag = if ($s.warnings? | default 0) > 0 { $" ⚠ ($s.warnings) warnings" } else { "" }
print $" ($mark) ($s.id)($warn_tag)"
}
print ""
}
}
# Verify a run is complete and emit side effects (backlog close, artifacts summary).
# Fails with a specific message if any required step is missing or blocked.
export def "mode complete" [
mode: string, # Mode ID
--task (-t): string = "", # Backlog task ID to close on success
--run (-r): string = "", # Override run ID (default: active run)
--fmt (-f): string = "",
]: nothing -> nothing {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
# Resolve run context
let current = (load-current-run)
let run_id = if ($run | is-not-empty) { $run } else if ($current | is-not-empty) and ($current.mode? | default "") == $mode { $current.run_id? | default "" } else { "" }
if ($run_id | is-empty) {
error make { msg: $"No active run for mode '($mode)'. Start one with: ontoref run start ($mode)" }
}
let dir = $"(runs-dir)/($run_id)"
if not ($dir | path exists) {
error make { msg: $"Run directory not found: ($dir)" }
}
# Load mode DAG and reported steps
let mode_data = (load-mode-dag $mode)
let all_steps = ($mode_data.steps? | default [])
let reported = (load-run-steps $run_id)
# Build verification results — one record per step
let results = ($all_steps | each { |step_def|
let rep_list = ($reported | where { |r| $r.step == $step_def.id })
let rep = if ($rep_list | is-not-empty) { $rep_list | first } else { null }
let strategy = ($step_def.on_error?.strategy? | default "Stop")
{
id: $step_def.id,
reported: ($rep | is-not-empty),
status: (if ($rep | is-not-empty) { $rep.status } else { "pending" }),
strategy: $strategy,
# A step blocks completion if: not reported AND strategy=Stop,
# OR reported as fail AND strategy=Stop
blocks: (
(($rep | is-empty) and $strategy == "Stop") or
(($rep | is-not-empty) and $rep.status == "fail" and $strategy == "Stop")
),
}
})
let blockers = ($results | where blocks == true)
let warnings = ($results | where { |r| not $r.blocks and $r.status in ["fail", "pending"] })
let all_artifacts = ($reported | each { |r| $r.artifacts? | default [] } | flatten)
let total_warnings = ($reported | each { |r| $r.warnings? | default 0 } | math sum)
let resolved_task = if ($task | is-not-empty) { $task } else { $current.task? | default "" }
let ok = ($blockers | is-empty)
let result = {
run_id: $run_id,
mode: $mode,
task: $resolved_task,
ok: $ok,
steps_total: ($all_steps | length),
steps_reported: ($reported | length),
blockers: ($blockers | each { |b| { id: $b.id, status: $b.status, strategy: $b.strategy } }),
warnings: ($warnings | each { |w| { id: $w.id, status: $w.status } }),
artifacts: $all_artifacts,
total_warnings: $total_warnings,
}
if $ok {
# Mark run as completed in run.json
let run_meta = (open $"($dir)/run.json")
$run_meta | merge { completed_at: (now-iso), ok: true } | to json | save --force $"($dir)/run.json"
# Clear current.json — no active run
"{}" | save --force (current-run-file)
}
if $fmt == "json" {
print ($result | to json)
} else if $ok {
print ""
print $"(ansi green)✓ RUN COMPLETE(ansi reset): ($run_id)"
print "══════════════════════════════════════════════════════════════════"
print $" Mode: ($mode)"
if ($resolved_task | is-not-empty) { print $" Task: ($resolved_task)" }
print $" Steps: ($result.steps_reported)/($result.steps_total) reported"
if $total_warnings > 0 { print $" Warnings: ($total_warnings) advisory (non-blocking)" }
if ($all_artifacts | is-not-empty) {
print ""
print " Artifacts:"
for a in $all_artifacts { print $" ($a)" }
}
if ($resolved_task | is-not-empty) {
print ""
print $" Close backlog item with: ontoref backlog done ($resolved_task)"
}
print ""
} else {
print ""
print $"(ansi red)✗ RUN INCOMPLETE(ansi reset): ($run_id)"
print "══════════════════════════════════════════════════════════════════"
print $" ($blockers | length) blocker(s):"
for b in $blockers {
let reason = if $b.status == "pending" { "not reported" } else { $"($b.status) with on_error=Stop" }
print $" ($b.id) — ($reason)"
}
print ""
}
}