ontoref/reflection/bin/jjw.nu

324 lines
12 KiB
Text
Raw Normal View History

feat: domain extension system, VCS abstraction, personal/provisioning domains, web subpages Domain extension system (ADR-012): bash-layer dispatch activates repo_kind-conditional CLI domains. install.nu copies domains/ tree; short_alias wrappers generated (personal, prov). ore help and describe capabilities domain-aware. personal domain (PersonalOntology): career skills/talks/publications/positioning, CFP pipeline (Watching→Delivered), opportunities lifecycle, content pipeline, Sessionize integration. Daemon pages: /career, /personal. provisioning domain (DevWorkspace/Mixed): FSM state, next transitions, connections graph, gates, workspace card, capabilities, backlog. Daemon page: /provisioning. VCS abstraction layer (ADR-013): reflection/modules/vcs.nu — uniform jj/git API via filesystem detection (.jj/ vs .git/). opmode.nu and git-event.nu migrated off ^git. reflection/bin/jjw.nu — jj + ontoref + Radicle agent workspace lifecycle. jjw-ncl-merge.nu registered as jj merge tool for .ontology/ NCL conflicts. init-repo.nu for new_project mode. jj/rad not in ontoref requirements — belong in orchestration project manifests. 'Framework RepoKind: ontology/schemas/manifest.ncl gains 'Framework variant; ontoref self-identifies as framework — no domain activates for the protocol itself. Web presence: personal.html and provisioning.html domain subpages. index.html gains "Project Types — Domain Extensions" section with type cards and subpage links. Nav compacted (Arch/Prov labels, solid backdrop-filter background). on+re: vcs-abstraction (adrs: adr-013) and agent-workspace-orchestration Practice nodes; 21 manifest capabilities; state.ncl catalysts updated.
2026-04-07 23:08:29 +01:00
#!/usr/bin/env nu
# reflection/bin/jjw.nu — jj + ontoref + rad orchestration for agent workspaces.
#
# Wraps three CLIs (jj, ontoref, rad) into a coherent agent lifecycle:
# jjw agent create → workspace + ontoref run
# jjw agent status → list active workspaces and their run state
# jjw agent step → report a step in the ontoref run
# jjw agent publish → validate + push + open Radicle patch
# jjw agent merge → merge commit + advance main + cleanup
# jjw agent discard → abandon workspace and cancel run
#
# Requires: jj >= 0.40, ontoref in PATH, rad >= 1.0 (optional for publish)
#
# NCL auto-merge for .ontology/ conflicts:
# Register jjw-ncl-merge.nu as a jj merge tool in ~/.config/jj/config.toml:
# [merge-tools.ncl]
# program = "nu"
# args = ["{ONTOREF_ROOT}/reflection/bin/jjw-ncl-merge.nu", "$left", "$right", "$base", "$output"]
# Then resolve: jj resolve --tool ncl -- .ontology/core.ncl
#
# .ontoref-run metadata file lives in each workspace dir:
# .agents/<run_id>/.ontoref-run
#
# ONTOREF_ROOT must be set (done by `use env.nu *` in ontoref sessions).
# When called standalone, set ONTOREF_ROOT to the ontoref installation path.
const ONTOREF_RUN_FILE = ".ontoref-run"
const AGENTS_DIR = ".agents"
# ── Helpers ──────────────────────────────────────────────────────────────────
def project-root []: nothing -> string {
let pr = ($env.ONTOREF_PROJECT_ROOT? | default "")
if ($pr | is-not-empty) { $pr } else { pwd | path expand }
}
def agents-dir []: nothing -> string {
$"(project-root)/($AGENTS_DIR)"
}
def workspace-path [run_id: string]: nothing -> string {
$"(agents-dir)/($run_id)"
}
def run-file [run_id: string]: nothing -> string {
$"(workspace-path $run_id)/($ONTOREF_RUN_FILE)"
}
def ontoref-bin []: nothing -> string {
let root = ($env.ONTOREF_ROOT? | default "")
if ($root | is-not-empty) {
let p = $"($root)/ontoref"
if ($p | path exists) { return $p }
}
# Fall back to PATH
let which_r = do { ^which ontoref } | complete
if $which_r.exit_code == 0 { $which_r.stdout | str trim } else { "ontoref" }
}
def jj [args: list<string>]: nothing -> string {
let root = (project-root)
let r = do { ^jj --no-pager --repository $root ...$args } | complete
if $r.exit_code != 0 {
error make { msg: $"jj ($args | str join ' '): ($r.stderr | str trim)" }
}
$r.stdout
}
def onto [args: list<string>]: nothing -> string {
let bin = (ontoref-bin)
let r = do { ^nu $bin ...$args } | complete
if $r.exit_code != 0 {
error make { msg: $"ontoref ($args | str join ' '): ($r.stderr | str trim)" }
}
$r.stdout
}
def read-run-file [run_id: string]: nothing -> record {
let f = (run-file $run_id)
if not ($f | path exists) {
error make { msg: $"No .ontoref-run for workspace '($run_id)'" }
}
open $f | from json
}
def get-change-id [run_id: string]: nothing -> string {
let ws_name = $run_id
jj ["log", "--no-graph", "-r", $"($ws_name)@", "-T", "change_id ++ \"\\n\""] | str trim
}
def has-rad-remote []: nothing -> bool {
let r = do { ^jj --no-pager git remote list } | complete
if $r.exit_code != 0 { return false }
$r.stdout | lines | any { |l| ($l | str trim | str starts-with "rad") }
}
# ── Commands ─────────────────────────────────────────────────────────────────
# Create a new agent workspace tied to an ontoref run.
export def "agent create" [
agent_id: string, # Logical agent identifier (e.g. "architect-01")
--task (-t): string = "", # Backlog task ID to associate
--mode (-m): string = "sync-ontology",
]: nothing -> nothing {
let root = (project-root)
let agents = (agents-dir)
mkdir $agents
# Start ontoref run — get run_id from JSON output
let run_json = (onto ["run", "start", $mode, "--task", $task, "--fmt", "json"])
let run_data = ($run_json | from json)
let run_id = $run_data.run_id
let ws_path = (workspace-path $run_id)
let ws_path_str = $ws_path
# jj workspace add — workspace name = run_id (unique UUID-like string)
jj ["workspace", "add", $ws_path_str, "--name", $run_id] | ignore
let change_id = (get-change-id $run_id)
# Write .ontoref-run metadata
let meta = {
run_id: $run_id,
agent_id: $agent_id,
mode: $mode,
task: $task,
workspace_name: $run_id,
workspace_path: $ws_path_str,
change_id: $change_id,
created_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ"),
remote: (if (has-rad-remote) { "rad" } else { "origin" }),
}
$meta | to json | save --force (run-file $run_id)
print $" workspace: ($ws_path_str)"
print $" run_id: ($run_id)"
print $" change_id: ($change_id)"
}
# List active agent workspaces and their ontoref run state.
export def "agent status" [
agent_id?: string, # Filter by agent_id (optional)
]: nothing -> nothing {
let agents = (agents-dir)
if not ($agents | path exists) {
print " no active agent workspaces"
return
}
let entries = (ls $agents | where type == "dir")
if ($entries | is-empty) {
print " no active agent workspaces"
return
}
let rows = $entries | each { |e|
let run_id = ($e.name | path basename)
let rf = $"($e.name)/($ONTOREF_RUN_FILE)"
if not ($rf | path exists) { return null }
let meta = (open $rf | from json)
if ($agent_id | is-not-empty) and ($meta.agent_id != $agent_id) { return null }
let change_id = (
do { jj ["log", "--no-graph", "-r", $"($run_id)@", "-T", "change_id ++ \"\\n\""] }
| complete | if $in.exit_code == 0 { $in.stdout | str trim } else { "unknown" }
)
{
run_id: $run_id,
agent_id: $meta.agent_id,
mode: $meta.mode,
task: $meta.task,
change_id: $change_id,
created: $meta.created_at,
}
} | where { |r| $r != null }
if ($rows | is-empty) {
print $" no workspaces for agent ($agent_id? | default "any")"
} else {
print ($rows | table)
}
}
# Report a step in the ontoref run for an agent workspace.
export def "agent step" [
run_id: string, # Workspace / run ID
step_id: string, # Step to report
--status (-s): string = "", # pass | fail | skip
]: nothing -> nothing {
if ($status | is-empty) or not ($status in ["pass", "fail", "skip"]) {
error make { msg: $"--status required: pass | fail | skip (got: '($status)')" }
}
let meta = (read-run-file $run_id)
onto ["step", "report", $meta.mode, $step_id, "--status", $status] | ignore
print $" ($run_id) step ($step_id) → ($status)"
}
# Validate, push, and open a Radicle patch (or git push) for an agent workspace.
export def "agent publish" [
run_id: string,
]: nothing -> nothing {
let meta = (read-run-file $run_id)
# 1. Ontology drift check
onto ["sync", "diff", "--fail-on-drift"] | ignore
# 2. Constraint validation
onto ["validate", "check-all"] | ignore
# 3. Verify all mode steps are reported
onto ["mode", "complete", $meta.mode] | ignore
# 4. Set commit description
let desc = $"agent(($meta.agent_id)): ($meta.task)"
jj ["describe", "-m", $desc, "-r", $meta.change_id] | ignore
# 5. Create/update bookmark — `set` is idempotent (safe on re-push after amend)
let bookmark = $"agent/($meta.agent_id)/($run_id)"
jj ["bookmark", "set", $bookmark, "-r", $meta.change_id] | ignore
# 6. Push
let remote = $meta.remote
jj ["git", "push", "--remote", $remote, "--bookmark", $bookmark] | ignore
# 7. Open Radicle patch only if rad remote is configured
if $remote == "rad" {
let r = do { ^rad patch open --title $desc } | complete
if $r.exit_code != 0 {
print $" warning: rad patch open failed: ($r.stderr | str trim)"
} else {
print $" patch opened on Radicle"
}
} else {
print $" pushed to ($remote) — no Radicle remote, skipped rad patch open"
let modified = (jj ["diff", "--name-only", "-r", $meta.change_id])
if ($modified | lines | any { |l| $l | str starts-with ".ontology/" }) {
print $" WARNING: workspace modified .ontology/ files but remote is git-only."
print $" merge may be rejected by deny_ontology_writes on the server."
}
}
print $" published: ($bookmark) → ($remote)"
}
# Merge workspace into main, advance bookmark, and clean up.
export def "agent merge" [
run_id: string,
]: nothing -> nothing {
let meta = (read-run-file $run_id)
let change_id = $meta.change_id
# 1. Create merge commit (no-edit — doesn't touch working copies)
jj ["new", "--no-edit", "main", $change_id] | ignore
# 2. Find the merge commit: heads of change_id's descendants, excluding
# change_id itself. `heads(change_id::)` alone can match change_id if it
# is a head in the workspace — `~ $change_id` excludes the original commit.
let merge_revset = $"heads(($change_id)::) ~ ($change_id)"
# 3. Check for conflicts — only block on .ontology/ conflicts
let conflict_out = (jj ["log", "--no-graph", "-r", $merge_revset,
"-T", "if(conflict, \"CONFLICT\", \"CLEAN\") ++ \"\\n\""])
if ($conflict_out | str trim) == "CONFLICT" {
let files_out = (
do { jj ["diff", "--name-only", "-r", $merge_revset] }
| complete | get -o result | default ""
)
let onto_conflicts = ($files_out | lines | where { |l| $l | str starts-with ".ontology/" })
if ($onto_conflicts | is-not-empty) {
# Abandon the conflicted merge commit before aborting
do { jj ["abandon", $merge_revset] } | complete | ignore
error make {
msg: $".ontology/ conflicts require NCL merge resolution: ($onto_conflicts | str join ', ')"
}
}
print $" WARNING: merge commit has non-ontology conflicts (stored as jj data)"
print $" use `jj resolve -r ($merge_revset)` in workspace to clean up"
}
# 4. Advance main bookmark to the merge commit
jj ["bookmark", "set", "main", "-r", $merge_revset] | ignore
# 5. Dispatch VCS event so ontoref syncs
let hooks = $"($env.ONTOREF_ROOT? | default '')/reflection/hooks/git-event.nu"
if ($hooks | path exists) {
do { ^nu -c $"use ($hooks) *; on-vcs-event 'post-merge'" } | complete | ignore
}
# 6. Forget workspace and remove directory
do { ^jj --no-pager workspace forget $run_id } | complete | ignore
let ws_path = (workspace-path $run_id)
if ($ws_path | path exists) {
rm -rf $ws_path
}
print $" merged ($run_id) into main, workspace removed"
}
# Abandon an agent workspace without merging. Cancels the ontoref run.
export def "agent discard" [
run_id: string,
]: nothing -> nothing {
# Forget from jj
do { ^jj --no-pager workspace forget $run_id } | complete | ignore
# Remove workspace directory
let ws_path = (workspace-path $run_id)
if ($ws_path | path exists) {
rm -rf $ws_path
}
# Cancel the ontoref run (best-effort — run cancel may not exist in all versions)
let r = do { onto ["run", "cancel", $run_id] } | complete
if $r.exit_code != 0 {
print $" advisory: ontoref run cancel returned non-zero (run may already be complete)"
}
print $" discarded workspace ($run_id)"
}