chore: add ontology and reflection
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

This commit is contained in:
Jesús Pérez 2026-03-13 00:21:04 +00:00
parent 0436a3b436
commit 0396e4037b
Signed by: jesus
GPG Key ID: 9F243E355E0BC939
102 changed files with 15167 additions and 0 deletions

View File

@ -0,0 +1,7 @@
let s = import "../reflection/schemas/connections" in
{
upstream = [],
downstream = [],
peers = [],
} | s.Connections

254
.ontology/core.ncl Normal file
View File

@ -0,0 +1,254 @@
let d = import "../ontology/defaults/core.ncl" in
{
nodes = [
# ── Axioms (invariant = true) ─────────────────────────────────────────────
d.make_node {
id = "protocol-not-runtime",
name = "Protocol, Not Runtime",
pole = 'Yang,
level = 'Axiom,
description = "Onref is a protocol specification and tooling layer. It is never a runtime dependency. Projects implement the protocol; onref provides the schemas and modules to do so.",
invariant = true,
},
d.make_node {
id = "self-describing",
name = "Self-Describing",
pole = 'Yang,
level = 'Axiom,
description = "Onref describes itself using its own protocol. The .ontology/, adrs/, and reflection/ directories in this repository are onref consuming ontoref.",
invariant = true,
artifact_paths = [".ontology/core.ncl", ".ontology/state.ncl", "adrs/"],
},
d.make_node {
id = "no-enforcement",
name = "No Enforcement",
pole = 'Yang,
level = 'Axiom,
description = "Onref defines contracts and patterns. There is no enforcement mechanism. Coherence is voluntary and emerges from justified adoption.",
invariant = true,
},
d.make_node {
id = "dag-formalized",
name = "DAG-Formalized Knowledge",
pole = 'Yin,
level = 'Axiom,
description = "All project knowledge — concepts, tensions, decisions, state — is formalized as directed acyclic graphs. This enables transversal queries, impact analysis, and ecosystem-level visibility.",
invariant = true,
artifact_paths = ["ontology/schemas/", "crates/ontoref-ontology/"],
},
# ── Tensions ──────────────────────────────────────────────────────────────
d.make_node {
id = "formalization-vs-adoption",
name = "Formalization vs Adoption Friction",
pole = 'Spiral,
level = 'Tension,
description = "Richer formalization produces better ecosystem visibility but increases the cost of adoption. The balance: schemas are optional layers, not mandatory gates.",
},
d.make_node {
id = "ontology-vs-reflection",
name = "Ontology vs Reflection",
pole = 'Spiral,
level = 'Tension,
description = "Ontology captures what IS (invariants, structure, being). Reflection captures what BECOMES (operations, drift, memory). Both must coexist without one dominating. This tension is onref's core identity.",
},
# ── Practices ─────────────────────────────────────────────────────────────
d.make_node {
id = "adr-lifecycle",
name = "ADR Lifecycle",
pole = 'Yang,
level = 'Practice,
description = "Architectural decisions follow: Proposed → Accepted → Superseded. Superseded ADRs retain constraints for historical reconstruction. Active Hard constraints drive the constraint set.",
artifact_paths = [
"adrs/schema.ncl",
"adrs/reflection.ncl",
"adrs/_template.ncl",
"adrs/adr-001-protocol-as-standalone-project.ncl",
"adrs/adr-002-daemon-for-caching-and-notification-barrier.ncl",
"CHANGELOG.md",
],
},
d.make_node {
id = "reflection-modes",
name = "Reflection Modes",
pole = 'Yang,
level = 'Practice,
description = "Operational procedures are first-class artifacts encoded as NCL DAG contracts. Modes declare actors, steps, dependencies, and error strategies — not prose.",
artifact_paths = ["reflection/modes/", "reflection/schemas/", "crates/ontoref-reflection/"],
},
d.make_node {
id = "coder-process-memory",
name = "Coder Process Memory",
pole = 'Yin,
level = 'Practice,
description = "Session knowledge is captured as structured JSONL via coder record. Queryable, exportable, and promotable to reflection/knowledge/. The memory layer between sessions.",
artifact_paths = ["reflection/modules/coder.nu", "reflection/schemas/coder.ncl"],
},
d.make_node {
id = "describe-query-layer",
name = "Describe Query Layer",
pole = 'Yang,
level = 'Practice,
description = "describe.nu aggregates all project sources and answers self-knowledge queries: what IS this, what can I DO, what can I NOT do, what tools exist, what is the impact of changing X.",
artifact_paths = ["reflection/modules/describe.nu"],
},
d.make_node {
id = "ontoref-ontology-crate",
name = "Ontoref Ontology Crate",
pole = 'Yang,
level = 'Practice,
description = "Rust implementation for loading and querying .ontology/ NCL files as typed structs. Provides the Core, Gate, and State types for ecosystem-level introspection.",
artifact_paths = ["crates/ontoref-ontology/"],
},
d.make_node {
id = "ontoref-reflection-crate",
name = "Ontoref Reflection Crate",
pole = 'Yang,
level = 'Practice,
description = "Rust implementation for loading, validating, and executing Reflection modes as NCL DAG contracts against project state.",
artifact_paths = ["crates/ontoref-reflection/"],
},
d.make_node {
id = "adopt-ontoref-tooling",
name = "Adopt Ontoref Tooling",
pole = 'Yang,
level = 'Practice,
description = "Migration system for onboarding existing projects into the ontoref protocol. Provides .ontology/ stub templates, .ontoref/config.ncl template, scripts/ontoref thin wrapper, and the adopt_ontoref mode+form+script that wire everything up idempotently.",
artifact_paths = [
"templates/ontology/",
"templates/ontoref-config.ncl",
"templates/scripts-ontoref",
"reflection/modes/adopt_ontoref.ncl",
"reflection/forms/adopt_ontoref.ncl",
"reflection/templates/adopt_ontoref.nu.j2",
],
},
d.make_node {
id = "web-presence",
name = "Web Presence",
pole = 'Yang,
level = 'Practice,
description = "Landing page at assets/web/ describing the ontoref protocol to external audiences. Bilingual (EN/ES), covers protocol layers, yin/yang duality, crates, and adoption path. Self-description artifact.",
artifact_paths = ["assets/web/src/index.html", "assets/web/index.html", "README.md", "assets/architecture.svg"],
},
d.make_node {
id = "ontoref-daemon",
name = "Ontoref Daemon",
pole = 'Yang,
level = 'Practice,
description = "Runtime support daemon for the ontoref protocol. Provides NCL export caching, file watching, actor registry, notification barrier, HTTP API, MCP server (stdio + streamable-HTTP), Q&A NCL persistence, quick-actions catalog, and passive drift observation. Reads .ontoref/config.ncl at startup.",
invariant = false,
artifact_paths = [
"crates/ontoref-daemon/",
"reflection/modules/services.nu",
"crates/ontoref-daemon/src/ui/qa_ncl.rs",
"crates/ontoref-daemon/src/ui/drift_watcher.rs",
"crates/ontoref-daemon/src/mcp/mod.rs",
],
},
d.make_node {
id = "qa-knowledge-store",
name = "Q&A Knowledge Store",
pole = 'Yin,
level = 'Practice,
description = "Accumulated Q&A entries persisted as NCL — questions and answers captured during development sessions, AI interactions, and architectural reviews. Git-versioned, typed by QaEntry schema, queryable via MCP (ontoref_qa_list/add) and HTTP (/qa-json). Bridges session boundaries: knowledge is never lost between actor sessions.",
artifact_paths = [
"reflection/qa.ncl",
"reflection/schemas/qa.ncl",
"crates/ontoref-daemon/src/ui/qa_ncl.rs",
],
},
d.make_node {
id = "quick-actions",
name = "Quick Actions Catalog",
pole = 'Yang,
level = 'Practice,
description = "Runnable shortcuts over existing reflection modes. Configured as quick_actions in .ontoref/config.ncl (id, label, icon, category, mode, actors). Accessible from UI (/actions), CLI (./ontoref), and MCP (ontoref_action_list/add). New modes created via ontoref_action_add are immediately available as actions. Reduces friction between knowing a mode exists and executing it.",
artifact_paths = [
".ontoref/config.ncl",
"crates/ontoref-daemon/templates/pages/actions.html",
"reflection/modes/",
],
},
d.make_node {
id = "drift-observation",
name = "Passive Drift Observation",
pole = 'Spiral,
level = 'Practice,
description = "Background observer that bridges Yang code artifacts with Yin ontology declarations. Watches crates/, .ontology/, adrs/, reflection/modes/ for changes; after a debounce window runs sync scan + sync diff; if MISSING/STALE/DRIFT/BROKEN items are found emits an ontology_drift notification. Never applies changes automatically — apply remains a deliberate human or agent act.",
artifact_paths = [
"crates/ontoref-daemon/src/ui/drift_watcher.rs",
"reflection/modes/sync-ontology.ncl",
],
},
],
edges = [
{ from = "self-describing", to = "dag-formalized", kind = 'ManifestsIn, weight = 'High },
{ from = "self-describing", to = "adr-lifecycle", kind = 'ManifestsIn, weight = 'High },
{ from = "self-describing", to = "reflection-modes", kind = 'ManifestsIn, weight = 'High },
{ from = "ontology-vs-reflection", to = "dag-formalized", kind = 'Resolves, weight = 'High },
{ from = "ontology-vs-reflection", to = "coder-process-memory", kind = 'Resolves, weight = 'Medium },
{ from = "dag-formalized", to = "ontoref-ontology-crate", kind = 'ManifestsIn, weight = 'High },
{ from = "reflection-modes", to = "ontoref-reflection-crate", kind = 'ManifestsIn, weight = 'High },
{ from = "no-enforcement", to = "formalization-vs-adoption", kind = 'Resolves, weight = 'Medium },
{ from = "protocol-not-runtime", to = "no-enforcement", kind = 'Implies, weight = 'High },
{ from = "adr-lifecycle", to = "reflection-modes", kind = 'Complements, weight = 'Medium },
{ from = "describe-query-layer", to = "dag-formalized", kind = 'DependsOn, weight = 'High },
{ from = "coder-process-memory", to = "describe-query-layer", kind = 'Complements, weight = 'Medium },
{ from = "ontoref-daemon", to = "ontoref-ontology-crate", kind = 'Complements, weight = 'High },
{ from = "ontoref-daemon", to = "reflection-modes", kind = 'Complements, weight = 'Medium },
{ from = "protocol-not-runtime", to = "ontoref-daemon", kind = 'Contradicts, weight = 'Low,
note = "Daemon is optional runtime support, not a protocol requirement. Protocol functions without it." },
{ from = "no-enforcement", to = "adopt-ontoref-tooling", kind = 'ManifestsIn, weight = 'High,
note = "Adoption is voluntary — the tooling makes it easy but never mandatory." },
{ from = "adopt-ontoref-tooling", to = "reflection-modes", kind = 'DependsOn, weight = 'High },
{ from = "self-describing", to = "web-presence", kind = 'ManifestsIn, weight = 'Medium },
{ from = "web-presence", to = "adopt-ontoref-tooling", kind = 'Complements, weight = 'Medium },
# Q&A Knowledge Store edges
{ from = "qa-knowledge-store", to = "dag-formalized", kind = 'ManifestsIn, weight = 'High,
note = "Q&A entries are typed NCL records, git-versioned — knowledge as DAG." },
{ from = "qa-knowledge-store", to = "coder-process-memory", kind = 'Complements, weight = 'High,
note = "Q&A is the persistent layer; coder.nu is the session capture layer. Together they form the full memory stack." },
{ from = "ontoref-daemon", to = "qa-knowledge-store", kind = 'Contains, weight = 'High },
# Quick Actions edges
{ from = "quick-actions", to = "reflection-modes", kind = 'DependsOn, weight = 'High,
note = "Each quick action invokes a reflection mode by id." },
{ from = "quick-actions", to = "ontoref-daemon", kind = 'ManifestsIn, weight = 'Medium },
{ from = "describe-query-layer", to = "quick-actions", kind = 'Complements, weight = 'Medium,
note = "describe capabilities lists available modes; quick-actions makes them executable." },
# Drift Observation edges
{ from = "drift-observation", to = "ontoref-daemon", kind = 'ManifestsIn, weight = 'High },
{ from = "drift-observation", to = "ontology-vs-reflection", kind = 'Resolves, weight = 'Medium,
note = "Drift observer continuously monitors the gap between Yin (ontology) and Yang (code). Passive resolution — it signals drift without forcing resolution." },
{ from = "drift-observation", to = "reflection-modes", kind = 'DependsOn, weight = 'High,
note = "Invokes sync-ontology mode steps (scan, diff) as read-only sub-processes." },
],
}

26
.ontology/gate.ncl Normal file
View File

@ -0,0 +1,26 @@
let d = import "../ontology/defaults/gate.ncl" in
{
membranes = [
d.make_membrane {
id = "protocol-adoption-gate",
name = "Protocol Adoption Gate",
description = "Controls which projects are recognized as onref-compliant. A project passes the gate when its .ontology/ validates against the onref schema contract.",
permeability = 'Low,
accepts = ['EcosystemRelevance],
protects = ["protocol stability", "schema versioning guarantees"],
opening_condition = {
max_tension_dimensions = 2,
pending_transitions = 3,
core_stable = true,
description = "Project implements the full onref schema contract and exports a valid .ontology/core.ncl.",
},
closing_condition = "When the project's .ontology/ fails schema validation after a breaking protocol change.",
max_duration = 'Indefinite,
protocol = 'Observe,
active = false,
},
],
}

54
.ontology/manifest.ncl Normal file
View File

@ -0,0 +1,54 @@
let m = import "../ontology/defaults/manifest.ncl" in
m.make_manifest {
project = "ontoref",
repo_kind = 'DevWorkspace,
consumption_modes = [
m.make_consumption_mode {
consumer = 'Developer,
needs = ['OntologyExport],
audit_level = 'Standard,
description = "Clones repo, runs ./ontoref, imports Nushell modules. Uses reflection tooling to manage project self-description.",
},
m.make_consumption_mode {
consumer = 'Agent,
needs = ['OntologyExport, 'JsonSchema],
audit_level = 'Quick,
description = "Reads .ontology/core.ncl via nickel export. Queries nodes, edges, and ADRs to understand project constraints before acting.",
},
],
layers = [
m.make_layer {
id = "protocol",
paths = ["ontology/", "adrs/", "reflection/schemas/"],
committed = true,
description = "Protocol specification: Nickel schemas, ADR tooling, and reflection schemas. The contract layer that projects implement.",
},
m.make_layer {
id = "tooling",
paths = ["reflection/", "ontoref"],
committed = true,
description = "Operational tooling: Nushell modules, modes, forms, dispatcher, and bash entry point.",
},
m.make_layer {
id = "crates",
paths = ["crates/", "Cargo.toml"],
committed = true,
description = "Rust implementation: ontoref-ontology (load/query .ontology/ as typed structs) and ontoref-reflection (execute reflection modes against project state).",
},
m.make_layer {
id = "self-description",
paths = [".ontology/"],
committed = true,
description = "Ontoref consuming ontoref: the project's own ontology, state, gate, and manifest.",
},
m.make_layer {
id = "process",
paths = [".coder/"],
committed = false,
description = "Session artifacts: plans, investigations, summaries. Process memory for actors.",
},
],
}

92
.ontology/state.ncl Normal file
View File

@ -0,0 +1,92 @@
let d = import "../ontology/defaults/state.ncl" in
{
dimensions = [
d.make_dimension {
id = "protocol-maturity",
name = "Protocol Maturity",
description = "Completeness of the ontoref protocol specification — schemas, ADRs, modes, Rust crates, daemon, and adoption tooling.",
current_state = "adoption-tooling-complete",
desired_state = "protocol-stable",
horizon = 'Months,
states = [],
transitions = [
d.make_transition {
from = "tooling-migrated",
to = "adoption-tooling-complete",
condition = "adopt_ontoref mode, templates, daemon crate, landing page all present and validated.",
catalyst = "Daemon extracted from stratumiops; adoption templates created.",
blocker = "none",
horizon = 'Months,
},
d.make_transition {
from = "adoption-tooling-complete",
to = "protocol-stable",
condition = "ADR-001 accepted, ontoref.dev published, at least two external projects consuming the protocol.",
catalyst = "First external adoption.",
blocker = "ontoref.dev not yet published; no external consumers yet.",
horizon = 'Months,
},
],
},
d.make_dimension {
id = "self-description-coverage",
name = "Self-Description Coverage",
description = "How completely ontoref describes itself using its own protocol.",
current_state = "modes-and-web-present",
desired_state = "fully-self-described",
horizon = 'Weeks,
states = [],
transitions = [
d.make_transition {
from = ".ontology-bootstrapped",
to = "modes-and-web-present",
condition = "adopt_ontoref mode, landing page, and all core.ncl nodes reflect current artifact set.",
catalyst = "Web presence and adoption tooling added in session 2026-03-12.",
blocker = "none",
horizon = 'Weeks,
},
d.make_transition {
from = "modes-and-web-present",
to = "fully-self-described",
condition = "At least 3 ADRs accepted, reflection/backlog.ncl present, describe project returns complete picture.",
catalyst = "ADR-001 and ADR-002 authored using ontoref against itself in session 2026-03-12.",
blocker = "1 more ADR needed to reach 3. reflection/backlog.ncl not present.",
horizon = 'Weeks,
},
],
},
d.make_dimension {
id = "ecosystem-integration",
name = "Ecosystem Integration",
description = "Degree to which other ecosystem projects (stratumiops, syntaxis, vapora, kogral) consume the ontoref protocol.",
current_state = "stratumiops-integrated",
desired_state = "multi-project",
horizon = 'Months,
coupled_with = ["protocol-maturity"],
states = [],
transitions = [
d.make_transition {
from = "source-only",
to = "stratumiops-integrated",
condition = "stratumiops has .ontoref/config.ncl and scripts/ontoref wrapper functional; ADR-007 marked Superseded pointing to ontoref:adr-002.",
catalyst = "Ontoref extraction and stratumiops migration session 2026-03-12.",
blocker = "none",
horizon = 'Months,
},
d.make_transition {
from = "stratumiops-integrated",
to = "multi-project",
condition = "At least one additional project (vapora, kogral, or syntaxis) has .ontoref/config.ncl and scripts/ontoref. Syntaxis parses ontoref Core type.",
catalyst = "Syntaxis integration spike or vapora/kogral onboarding.",
blocker = "Syntaxis syntaxis-ontology crate has ES→EN migration errors pending. vapora/kogral not yet initialized with .ontoref/.",
horizon = 'Months,
},
],
},
],
}

69
.ontoref/config.ncl Normal file
View File

@ -0,0 +1,69 @@
{
nickel_import_paths = [".", ".ontology", "ontology/schemas", "adrs", "reflection/requirements", "reflection/schemas"],
ui = {
templates_dir = "crates/ontoref-daemon/templates",
public_dir = "crates/ontoref-daemon/public",
tls_cert = "",
tls_key = "",
logo = "ontoref-logo.svg",
},
log = {
level = "info",
path = ".ontoref/logs",
rotation = "daily",
compress = false,
archive = ".ontoref/logs/archive",
max_files = 7,
},
mode_run = {
rules = [
{ when = { mode_id = "validate-ontology" }, allow = true, reason = "validation always allowed" },
{ when = { actor = "agent" }, allow = true, reason = "agent actor always allowed" },
{ when = { actor = "ci" }, allow = true, reason = "ci actor always allowed" },
],
},
nats_events = {
enabled = false,
url = "nats://localhost:4222",
emit = [],
subscribe = [],
handlers_dir = "reflection/handlers",
},
actor_init = [
{ actor = "agent", mode = "describe capabilities", auto_run = true },
{ actor = "developer", mode = "", auto_run = false },
{ actor = "ci", mode = "", auto_run = false },
],
quick_actions = [
{
id = "gen-docs",
label = "Generate documentation",
icon = "book-open",
category = "docs",
mode = "generate-mdbook",
actors = ["developer", "agent"],
},
{
id = "sync-onto",
label = "Sync ontology",
icon = "refresh",
category = "sync",
mode = "sync-ontology",
actors = ["developer", "ci", "agent"],
},
{
id = "coder-workflow",
label = "Coder workflow",
icon = "code",
category = "process",
mode = "coder-workflow",
actors = ["developer", "agent"],
},
],
}

38
.ontoref/logs Normal file
View File

@ -0,0 +1,38 @@
{"ts":"2026-03-12T02:15:07+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T02:58:09+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T03:04:52+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T03:05:21+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T03:05:58+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T03:06:03+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T03:11:03+0000","author":"unknown","actor":"agent","level":"read","action":"adr list"}
{"ts":"2026-03-12T03:12:05+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T03:12:19+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T03:47:17+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T06:41:52+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T06:41:59+0000","author":"unknown","actor":"developer","level":"read","action":"adr list"}
{"ts":"2026-03-12T08:50:18+0000","author":"unknown","actor":"developer","level":"read","action":"constraint"}
{"ts":"2026-03-12T08:51:26+0000","author":"unknown","actor":"developer","level":"read","action":"overview"}
{"ts":"2026-03-12T08:52:04+0000","author":"unknown","actor":"developer","level":"read","action":"about"}
{"ts":"2026-03-12T08:52:23+0000","author":"unknown","actor":"developer","level":"read","action":"about"}
{"ts":"2026-03-12T08:52:44+0000","author":"unknown","actor":"developer","level":"read","action":"status"}
{"ts":"2026-03-12T10:20:47+0000","author":"unknown","actor":"developer","level":"read","action":"describe project"}
{"ts":"2026-03-12T18:29:08+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T18:33:17+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T18:33:32+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T18:33:36+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T18:39:22+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T18:39:26+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T18:39:41+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T18:39:45+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T18:40:33+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T18:40:37+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T18:41:04+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T18:41:07+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T18:43:22+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T18:43:25+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T18:43:43+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T18:43:47+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T20:46:07+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T20:46:12+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}
{"ts":"2026-03-12T20:47:05+0000","author":"unknown","actor":"developer","level":"read","action":"sync scan"}
{"ts":"2026-03-12T20:47:09+0000","author":"unknown","actor":"developer","level":"read","action":"sync diff"}

7
.ontoref/registry.toml Normal file
View File

@ -0,0 +1,7 @@
[[projects]]
slug = "ontoref"
root = "/Users/Akasha/Development/ontoref"
[[projects]]
slug = "typedialog"
root = "/Users/Akasha/Development/typedialog"

103
.ontoref/roles.ncl Normal file
View File

@ -0,0 +1,103 @@
# Actor session roles — typed contract for role definitions used by the
# ontoref daemon actor registry.
#
# The `role` field in ActorSession is validated against this file when present.
# A role defines which UI capabilities are granted and what UI defaults apply.
#
# Load example:
# nickel export --format json .ontoref/roles.ncl
let permission_type = [|
'read_backlog,
'write_backlog,
'read_adrs,
'write_adrs,
'run_modes,
'emit_notifications,
'manage_projects,
'manage_sessions,
|] in
let nav_mode_type = [| 'icons, 'icons_text, 'text |] in
let theme_type = [| 'dark, 'light, 'system |] in
let role_def_type = {
id | String,
label | String,
description | String | default = "",
permissions | Array permission_type,
ui_defaults | {
theme | theme_type | default = 'system,
nav_mode | nav_mode_type | default = 'icons_text,
} | default = {},
} in
{
roles | Array role_def_type = [
{
id = "admin",
label = "Admin",
description = "Full access — manage projects, sessions, ADRs, backlog, and emit notifications.",
permissions = [
'read_backlog,
'write_backlog,
'read_adrs,
'write_adrs,
'run_modes,
'emit_notifications,
'manage_projects,
'manage_sessions,
],
ui_defaults = { theme = 'dark, nav_mode = 'icons_text },
},
{
id = "developer",
label = "Developer",
description = "Standard development access — read/write backlog and ADRs, run modes.",
permissions = [
'read_backlog,
'write_backlog,
'read_adrs,
'write_adrs,
'run_modes,
'emit_notifications,
],
ui_defaults = { theme = 'system, nav_mode = 'icons_text },
},
{
id = "viewer",
label = "Viewer",
description = "Read-only access — view backlog, ADRs, notifications.",
permissions = [
'read_backlog,
'read_adrs,
],
ui_defaults = { theme = 'system, nav_mode = 'icons },
},
{
id = "agent",
label = "Agent",
description = "Automated agent — run modes, read/write backlog, emit notifications.",
permissions = [
'read_backlog,
'write_backlog,
'read_adrs,
'run_modes,
'emit_notifications,
],
ui_defaults = { theme = 'dark, nav_mode = 'icons },
},
{
id = "ci",
label = "CI",
description = "Continuous integration actor — read backlog and ADRs, run modes.",
permissions = [
'read_backlog,
'read_adrs,
'run_modes,
],
ui_defaults = { theme = 'dark, nav_mode = 'icons },
},
],
}

51
adrs/_template.ncl Normal file
View File

@ -0,0 +1,51 @@
# ADR template — plain record for typedialog roundtrip input.
# No contracts applied here; contracts are enforced in the Jinja2 output template.
#
# Usage:
# typedialog nickel-roundtrip \
# --input adrs/_template.ncl \
# --form reflection/forms/new_adr.ncl \
# --output adrs/adr-NNN-title.ncl \
# --ncl-template reflection/templates/adr.ncl.j2
{
id = "adr-000",
title = "",
status = "Proposed",
date = "2026-03",
context = "",
decision = "",
rationale = [
{ claim = "", detail = "" },
],
consequences = {
positive = [""],
negative = [""],
},
alternatives_considered = [
{ option = "", why_rejected = "" },
],
constraints = [
{
id = "",
claim = "",
scope = "",
severity = "Hard",
check_hint = "",
rationale = "",
},
],
related_adrs = [],
ontology_check = {
decision_string = "",
invariants_at_risk = [],
verdict = "Safe",
},
}

View File

@ -0,0 +1,96 @@
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-001",
title = "Ontoref is a Standalone Protocol Project, Not Part of Stratumiops",
status = 'Accepted,
date = "2026-03-12",
context = "The ontology/reflection patterns originated inside stratumiops as self-description tooling (stratum-ontology-core, stratum-reflection-core, stratum-daemon). Consumer projects (typedialog, vapora, kogral) needed the same patterns but had no path to adoption that did not entangle them with stratumiops' pipeline-specific crates (stratum-graph, stratum-state, stratum-orchestrator). Protocol evolution (schema changes, ADR lifecycle, daemon features) was blocked behind stratumiops release cycles. The three crates were logically a specification layer that happened to live in the wrong repo.",
decision = "Ontoref is extracted as a standalone protocol project with independent versioning, CI, and crates: ontoref-ontology (Rust types for the ontology graph), ontoref-reflection (mode runner and schema validation), ontoref-daemon (NCL export cache, file watcher, actor registry). Consumer projects adopt the protocol via a thin `scripts/ontoref` bash wrapper and a `.ontoref/config.ncl` declaration. The ontoref project itself is allowed to use stratum-db and platform-nats as optional peer dependencies via workspace path references, since those crates are infrastructural rather than domain-specific.",
rationale = [
{
claim = "Protocol concerns are orthogonal to pipeline concerns",
detail = "Stratumiops' primary value is the action graph execution engine (stratum-orchestrator) and its pipeline crates. The ontology/ADR/reflection protocol serves any project — not just stratumiops consumers. Coupling forces every protocol adopter to also accept stratumiops' dependency tree.",
},
{
claim = "Independent versioning enables protocol stability",
detail = "Protocol schema changes (new ADR fields, new ontology edge kinds, new reflection mode types) should not require a stratumiops release. Ontoref's version contract is between ontoref and its consumers; stratumiops is one consumer, not the owner.",
},
{
claim = "ONTOREF_PROJECT_ROOT decouples protocol tooling from project location",
detail = "The ontoref entry point accepts ONTOREF_PROJECT_ROOT as an env var, defaulting to its own SCRIPT_DIR only if unset. Consumer wrappers (scripts/ontoref) set ONTOREF_PROJECT_ROOT to their own root before exec-ing the ontoref entry. This design allows one ontoref checkout to serve multiple projects.",
},
{
claim = "stratum-db and platform-nats as peer deps, not bundled deps",
detail = "ontoref-daemon uses stratum-db (for optional SurrealDB persistence) and platform-nats (for NATS events) as optional features via path dependencies. These crates are infrastructural and re-usable. Bundling them into ontoref would duplicate the crates; referencing them as peers preserves the single canonical implementation.",
},
],
consequences = {
positive = [
"Consumer projects (typedialog, vapora, kogral) can adopt the protocol with a single wrapper + config file",
"Protocol schema evolution is independent of stratumiops pipeline releases",
"One ontoref checkout serves all projects via ONTOREF_PROJECT_ROOT env var",
"Stratumiops becomes a first-class protocol consumer, not the protocol owner — eats its own dogfood",
"ontoref-daemon is protocol-agnostic: any project can use it for NCL caching regardless of whether it uses stratumiops",
],
negative = [
"ontoref-daemon has path dependencies on stratumiops/crates — requires both repos to be checked out locally for builds",
"Two entry points per project: scripts/stratumiops (pipeline) and scripts/ontoref (protocol) — dual maintenance surface",
"ONTOREF_ROOT must be set or defaulted correctly in every consumer wrapper",
],
},
alternatives_considered = [
{
option = "Keep all tooling in stratumiops, consumers depend on it as a git subtree or submodule",
why_rejected = "Submodule/subtree patterns create update friction and do not solve the versioning coupling. Every consumer needs stratumiops' full dependency tree even for protocol-only use.",
},
{
option = "Publish ontoref crates to crates.io and consume via version pins",
why_rejected = "Rapid iteration on protocol schemas makes published crate semantics too rigid at this stage. Path dependencies allow simultaneous development of protocol and consumers without publication ceremony.",
},
{
option = "Inline protocol tooling into each consumer project separately",
why_rejected = "Schema drift across projects would immediately arise. The protocol's value is precisely its shared contract — decentralizing it defeats the purpose.",
},
],
constraints = [
{
id = "no-stratumiops-domain-deps",
claim = "ontoref crates must not import stratumiops domain crates: stratum-graph, stratum-state, stratum-orchestrator, stratum-llm, stratum-embeddings",
scope = "crates/ontoref-ontology/Cargo.toml, crates/ontoref-reflection/Cargo.toml, crates/ontoref-daemon/Cargo.toml",
severity = 'Hard,
check_hint = "rg 'stratum-graph|stratum-state|stratum-orchestrator|stratum-llm|stratum-embeddings' crates/*/Cargo.toml",
rationale = "Domain crates from stratumiops encode pipeline-specific types. Importing them would re-couple the protocol to the pipeline and prevent independent adoption.",
},
{
id = "ontoref-project-root-consumer-set",
claim = "The ontoref entry point must not unconditionally overwrite ONTOREF_PROJECT_ROOT — it must default only when unset",
scope = "ontoref (bash entry point)",
severity = 'Hard,
check_hint = "grep 'ONTOREF_PROJECT_ROOT' ontoref | grep -v ':-'",
rationale = "Consumer wrappers (scripts/ontoref) set ONTOREF_PROJECT_ROOT to their own root before calling the ontoref entry. If the entry overwrites it, the daemon and ADR queries target ontoref's own repo instead of the consumer project.",
},
{
id = "ontoref-config-only-required-artifact",
claim = "A consumer project must only need .ontoref/config.ncl and scripts/ontoref to adopt the protocol — no other files copied into the consumer",
scope = "consumer project onboarding",
severity = 'Soft,
check_hint = "ls .ontoref/ scripts/ontoref",
rationale = "Minimizing the consumer adoption surface ensures the protocol is adopted voluntarily and fully, not partially via file copies that drift from the source.",
},
],
related_adrs = ["adr-002-daemon-for-caching-and-notification-barrier"],
ontology_check = {
decision_string = "ontoref is a standalone project providing protocol tooling; consumers adopt via wrapper + config; stratum-db and platform-nats are optional peer deps",
invariants_at_risk = ["protocol-not-runtime"],
verdict = 'Safe,
},
}

View File

@ -0,0 +1,114 @@
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-002",
title = "Ontoref Daemon for NCL Caching, File Watching, and Actor Notification Barrier",
status = 'Accepted,
date = "2026-03-12",
supersedes = "stratumiops:adr-007-optional-daemon-for-caching-and-persistence",
context = "Nushell reflection modules invoke `nickel export` as a subprocess ~39 times per full sync scan, taking 2m42s. Each invocation forks a new process (~100ms). There is no shared state between developers and agents working on the same project, no notification when a peer changes an ontology or ADR file mid-session, and no persistent store for scan results. The protocol-not-runtime axiom forbids required runtime services — any daemon must be optional with full subprocess fallback. This ADR supersedes stratumiops adr-007, which designed this system inside stratumiops before the protocol was extracted to ontoref.",
decision = "ontoref-daemon is an optional persistent daemon providing: (1) NCL export caching — results keyed by (path, mtime, import_path) served via HTTP, file watcher invalidates on change; (2) actor registry — developers, agents, CI register on startup with deterministic tokens (type:hostname:pid), sweep reaps stale sessions every 30s; (3) notification barrier — file changes in .ontology/, adrs/, reflection/ generate typed notifications stored in a per-project ring buffer; pre-commit hook queries pending notifications and blocks commits until acknowledged (fail-open: daemon down = commit allowed). Consumer projects configure the daemon via `.ontoref/config.ncl` (daemon.enabled, daemon.port, db.enabled, db.url). All Nushell modules fall back to direct `nickel export` subprocess when the daemon is unavailable. stratum-db (optional feature, path dep on stratumiops) handles SurrealDB persistence. platform-nats (optional feature, path dep on stratumiops) handles NATS event publishing.",
rationale = [
{
claim = "Optional daemon preserves protocol-not-runtime axiom",
detail = "Every HTTP call to the daemon has a subprocess fallback in store.nu. daemon-export-safe returns null on error; callers substitute with direct nickel export. A project with daemon.enabled = false works identically — just without caching. The daemon is an optimization layer, never a load-bearing dependency.",
},
{
claim = "NCL export caching eliminates repeated subprocess forks",
detail = "Daemon caches nickel export results keyed by (path, mtime, NICKEL_IMPORT_PATH). First call ~100ms (subprocess). Subsequent calls <1ms (DashMap lookup). File watcher via notify (FSEvents on macOS, inotify on Linux) invalidates on any change to watched paths. Periodic full invalidation as safety net.",
},
{
claim = "Actor registry enables multiactor awareness without coordination overhead",
detail = "Deterministic tokens (type:hostname:pid) require no UUID generation and are stable across restarts of the same process. Stale sessions are swept by kill -0 check (same-machine actors) or last_seen timeout (CI/remote actors). No heartbeat loop required from actors.",
},
{
claim = "Notification barrier is fail-open, preserving developer autonomy",
detail = "The pre-commit hook checks /notifications/pending and blocks only when notifications are pending AND the daemon is reachable. Daemon unavailable = commit allowed (warning printed). This matches the no-enforcement axiom: ontoref cannot enforce coordination, only facilitate it.",
},
{
claim = "stratum-db and platform-nats as optional peer path deps",
detail = "Both crates are infrastructure utilities with no domain coupling to stratumiops. Using them as optional path deps lets ontoref-daemon avoid duplicating SurrealDB and NATS connection logic. The `db` and `nats` features are enabled by default but can be disabled for protocol-only deployments.",
},
],
consequences = {
positive = [
"sync scan latency drops from ~2m42s to <30s with daemon caching active",
"Developers and agents see each other's pending ontology/ADR changes before committing",
"Pre-commit hook prevents silent drift when multiple actors modify shared protocol files",
"SurrealDB persistence (optional) enables cross-session impact analysis and audit history",
"Single daemon process serves multiple projects via X-Ontoref-Project header scoping",
],
negative = [
"ontoref-daemon has path deps on stratumiops/crates — both repos must be co-located for local builds",
"Daemon process uses memory (~10-50MB) while running; idle timeout mitigates this",
"Actor notification barrier adds pre-commit latency for the HTTP round-trip (~5ms daemon reachable)",
"stratum-db pins SurrealDB v3 — major version changes require coordinated update across both repos",
],
},
alternatives_considered = [
{
option = "In-process Nickel evaluation via nickel-lang-core library",
why_rejected = "nickel-lang-core has an unstable Rust API and resolves import paths differently from the CLI. The subprocess approach with caching is simpler, more stable, and already <1ms cached.",
},
{
option = "Required daemon (always running, no fallback)",
why_rejected = "Violates the protocol-not-runtime axiom. Consumer projects must function without any ontoref process running. The daemon is an optimization and awareness layer, not infrastructure.",
},
{
option = "Filesystem-based JSON cache without a daemon process",
why_rejected = "File-based caching requires lock management, cannot serve concurrent requests from multiple actors, and does not provide file watching or the actor registry. A daemon centralizes these concerns cleanly.",
},
{
option = "Bundle SurrealDB and NATS clients directly in ontoref, not as path deps",
why_rejected = "Duplicating stratum-db and platform-nats creates divergence. Both crates are general-purpose infrastructure; ontoref consumers already have stratumiops checked out for other reasons. Path deps preserve the single canonical implementation.",
},
],
constraints = [
{
id = "daemon-never-required",
claim = "No Nushell module or bash script may fail when ontoref-daemon is unavailable",
scope = "reflection/modules/, reflection/nulib/, scripts/",
severity = 'Hard,
check_hint = "rg -l 'daemon-export' reflection/modules/ reflection/nulib/ | xargs rg -L 'daemon-export-safe|subprocess fallback|nickel export'",
rationale = "Every daemon-export call site must have a subprocess fallback. Daemon down = system works identically, just slower.",
},
{
id = "daemon-binds-localhost-only",
claim = "ontoref-daemon must bind to 127.0.0.1, never to 0.0.0.0 or a public interface",
scope = "crates/ontoref-daemon/src/main.rs",
severity = 'Hard,
check_hint = "rg '0\\.0\\.0\\.0' crates/ontoref-daemon/src/main.rs",
rationale = "The daemon is local IPC only. Binding to a public interface would expose the NCL export API to the network.",
},
{
id = "notification-barrier-fail-open",
claim = "The pre-commit hook must allow commits when ontoref-daemon is unreachable, printing a warning but not blocking",
scope = "scripts/hooks/pre-commit-notifications.sh",
severity = 'Hard,
check_hint = "grep -A5 'daemon down\\|curl.*fail\\|unreachable' scripts/hooks/pre-commit-notifications.sh",
rationale = "A pre-commit hook that blocks on daemon unavailability violates the no-enforcement axiom and the developer autonomy principle. Coordination is facilitated, never enforced.",
},
{
id = "multi-project-header-scoping",
claim = "All daemon HTTP requests from consumer wrappers must include X-Ontoref-Project header or equivalent project scoping",
scope = "reflection/modules/store.nu, crates/ontoref-daemon/src/api.rs",
severity = 'Soft,
check_hint = "rg 'X-Ontoref-Project' reflection/modules/store.nu crates/ontoref-daemon/src/api.rs",
rationale = "One daemon process serves multiple projects. Without project scoping, notifications and cache entries from different projects would collide.",
},
],
related_adrs = ["adr-001-protocol-as-standalone-project"],
ontology_check = {
decision_string = "optional persistent daemon for NCL caching, actor registry, and notification barrier — never required, fail-open design throughout",
invariants_at_risk = ["protocol-not-runtime", "no-enforcement"],
verdict = 'Safe,
},
}

View File

@ -0,0 +1,100 @@
let d = import "defaults.ncl" in
d.make_adr {
id = "adr-003",
title = "Q&A and Accumulated Knowledge Persist to NCL, Not Browser Storage",
status = 'Accepted,
date = "2026-03-12",
context = "The initial Q&A bookmarks feature stored entries in browser localStorage keyed by project. This is convenient but violates the dag-formalized axiom: knowledge that lives only in a browser session is invisible to agents, not git-versioned, not queryable via MCP, and is lost on browser data reset. The same problem applies to quick actions and any other accumulated operational knowledge. The system accumulates knowledge during development sessions (AI interactions, architectural reviews, debugging) that should be first-class artifacts — not ephemeral browser state.",
decision = "All Q&A entries persist to `reflection/qa.ncl` — a typed NCL record file governed by `reflection/schemas/qa.ncl`. Mutations happen via `crates/ontoref-daemon/src/ui/qa_ncl.rs` (line-level surgery, same pattern as backlog_ncl.rs). Four MCP tools expose the store to AI agents: `ontoref_qa_list` (read, with filter), `ontoref_qa_add` (append), `ontoref_qa_delete` (remove block), `ontoref_qa_update` (field mutation). HTTP endpoints `/qa-json` (GET), `/qa/add`, `/qa/delete`, `/qa/update` (POST) serve the UI. The UI renders server-side entries via Tera template injection (SERVER_ENTRIES JSON blob), eliminating the need for a separate fetch on load. The same principle is applied to quick actions: they are declared in `.ontoref/config.ncl` (quick_actions array) and new modes are created as `reflection/modes/<id>.ncl` files via `ontoref_action_add`.",
rationale = [
{
claim = "NCL persistence makes knowledge git-versioned and queryable",
detail = "A Q&A entry in reflection/qa.ncl is a typed Nickel record. It survives browser resets, is visible in git history, can be diffed, and is exported via the NCL cache just like any other protocol artifact. localStorage provides none of these properties.",
},
{
claim = "MCP access enables AI agents to read and write accumulated knowledge",
detail = "Without server-side persistence, an AI agent cannot query what questions have been asked and answered about this project. With ontoref_qa_list, the agent can orient itself immediately from accumulated Q&A without re-asking questions already answered in previous sessions. With ontoref_qa_add, the agent can record newly discovered knowledge while working.",
},
{
claim = "Line-level NCL surgery avoids full AST parsing",
detail = "The qa_ncl.rs module uses the same pattern as backlog_ncl.rs: find blocks by id field, insert/replace/remove by line index. No nickel-lang-core dependency, no AST round-trip, no format drift. The NCL format is predictable enough that targeted string operations are safe and sufficient.",
},
{
claim = "Server-side hydration eliminates the initial fetch round-trip",
detail = "The qa.html template receives `entries` as a Tera context variable and embeds SERVER_ENTRIES as a JSON literal in the page script. The JS initialises its in-memory list directly from this value. No separate GET /qa-json fetch on load, no loading state, no flash of empty content.",
},
{
claim = "Passive drift observation closes the knowledge-accumulation loop",
detail = "As code changes, drift_watcher.rs detects divergence between Yang artifacts and Yin ontology and emits a notification. Combined with Q&A persistence, the system can accumulate observations about drift patterns — enabling future Q&A entries like 'why does crate X often drift?' to be answered from stored context.",
},
],
consequences = {
positive = [
"Q&A knowledge survives session boundaries — AI agents and developers share a common context store",
"MCP tools give AI clients direct read/write access to accumulated project knowledge",
"All entries are typed (QaEntry schema), git-diffable, and auditable",
"Quick actions in config.ncl are the canonical source — no UI state drift",
"Passive drift observer closes the feedback loop without requiring manual sync",
],
negative = [
"qa.ncl and backlog.ncl use line-level surgery — format changes to the NCL templates require updating the mutation modules",
"No concurrent write protection — simultaneous writes from multiple actors could corrupt qa.ncl (same limitation as backlog.ncl; mitigated by advisory file locks in the ./ontoref CLI)",
"Deletion removes the block permanently — no soft-delete or archive mechanism yet",
],
},
alternatives_considered = [
{
option = "Keep localStorage as the storage backend, add optional sync to NCL on explicit user action",
why_rejected = "Two sources of truth creates sync complexity and divergence. AI agents would still not see localStorage entries. The sync step would be frequently skipped. NCL as primary is simpler.",
},
{
option = "Store Q&A in SurrealDB via stratum-db (existing optional dependency)",
why_rejected = "Requires the db feature and a running SurrealDB instance. NCL files are always present, git-versioned, and work without any database. The protocol-not-runtime axiom argues for file-first. SurrealDB can be added as a secondary index later if full-text search is needed.",
},
{
option = "Full AST parse of qa.ncl via nickel-lang-core for mutations",
why_rejected = "nickel-lang-core has an unstable Rust API. The file structure is predictable enough for line-level surgery. backlog_ncl.rs has been operating safely with this pattern. Adding a hard dependency on nickel-lang-core for a file that is always written by the daemon is unnecessary complexity.",
},
],
constraints = [
{
id = "qa-write-via-mutation-module",
claim = "All mutations to reflection/qa.ncl must go through crates/ontoref-daemon/src/ui/qa_ncl.rs — no direct file writes from other call sites",
scope = "crates/ontoref-daemon/src/",
severity = 'Hard,
check_hint = "rg -l 'qa.ncl' crates/ontoref-daemon/src/ | rg -v 'qa_ncl.rs|handlers.rs|api.rs|mcp'",
rationale = "Centralising mutations in one module ensures consistent id generation, NCL format, and cache invalidation.",
},
{
id = "qa-schema-typed",
claim = "reflection/qa.ncl must conform to the QaStore contract from reflection/schemas/qa.ncl — nickel typecheck must pass",
scope = "reflection/qa.ncl",
severity = 'Hard,
check_hint = "nickel typecheck reflection/qa.ncl",
rationale = "Untyped Q&A would degrade to an unstructured log. The schema enforces id, question, answer, actor, created_at fields on every entry.",
},
{
id = "mcp-qa-tools-no-apply-drift",
claim = "MCP tools ontoref_qa_list and ontoref_qa_add must never trigger sync apply steps or modify .ontology/ files",
scope = "crates/ontoref-daemon/src/mcp/mod.rs",
severity = 'Hard,
check_hint = "rg -A20 'QaAddTool|QaListTool' crates/ontoref-daemon/src/mcp/mod.rs | rg -c 'apply|sync|ontology'",
rationale = "Q&A mutation tools operate only on reflection/qa.ncl. Ontology changes require deliberate human or agent review via the sync-ontology mode.",
},
],
related_adrs = ["adr-001-protocol-as-standalone-project", "adr-002-daemon-for-caching-and-notification-barrier"],
ontology_check = {
decision_string = "Q&A and operational knowledge persist as typed NCL artifacts, git-versioned, accessible via MCP and HTTP, rendered server-side in UI",
invariants_at_risk = ["dag-formalized", "protocol-not-runtime"],
verdict = 'Safe,
},
}

1
adrs/adr-base.ncl Normal file
View File

@ -0,0 +1 @@
import "adr-defaults.ncl"

51
adrs/adr-constraints.ncl Normal file
View File

@ -0,0 +1,51 @@
let _adr_id_format = std.contract.custom (
fun label =>
fun value =>
if std.string.is_match "^adr-[0-9]{3}$" value then
'Ok value
else
'Error {
message = "ADR id must match 'adr-NNN' format (e.g. 'adr-001'), got: '%{value}'"
}
) in
let _non_empty_constraints = std.contract.custom (
fun label =>
fun value =>
if std.array.length value == 0 then
'Error {
message = "constraints must not be empty — an ADR with no constraints is passive documentation, not an active constraint"
}
else
'Ok value
) in
let _non_empty_negative = std.contract.custom (
fun label =>
fun value =>
if std.array.length value.negative == 0 then
'Error {
message = "consequences.negative must not be empty on id='%{value.id}' — an ADR with no negative consequences is incomplete"
}
else
'Ok value
) in
let _requires_justification = std.contract.custom (
fun label =>
fun value =>
if value.ontology_check.verdict == 'RequiresJustification
&& !(std.record.has_field "invariant_justification" value) then
'Error {
message = "ADR '%{value.id}': ontology_check.verdict = 'RequiresJustification but invariant_justification field is missing"
}
else
'Ok value
) in
{
AdrIdFormat = _adr_id_format,
NonEmptyConstraints = _non_empty_constraints,
NonEmptyNegativeConsequences = _non_empty_negative,
RequiresJustificationWhenRisky = _requires_justification,
}

16
adrs/adr-defaults.ncl Normal file
View File

@ -0,0 +1,16 @@
let s = import "adr-schema.ncl" in
let c = import "adr-constraints.ncl" in
{
# RequiresJustificationWhenRisky is a cross-field contract (reads both
# ontology_check.verdict and invariant_justification) — applied here after
# the schema merge so both fields are visible in the same record.
make_adr = fun data =>
let result | c.RequiresJustificationWhenRisky = s.Adr & data in
result,
make_constraint = fun data => s.Constraint & data,
Adr = s.Adr,
Constraint = s.Constraint,
OntologyCheck = s.OntologyCheck,
}

74
adrs/adr-schema.ncl Normal file
View File

@ -0,0 +1,74 @@
let c = import "adr-constraints.ncl" in
let status_type = [| 'Proposed, 'Accepted, 'Superseded, 'Deprecated |] in
let severity_type = [| 'Hard, 'Soft |] in
let verdict_type = [| 'Safe, 'RequiresJustification |] in
let rationale_entry_type = {
claim | String,
detail | String,
} in
let alternative_type = {
option | String,
why_rejected | String,
} in
let constraint_type = {
id | String,
claim | String,
scope | String,
severity | severity_type,
check_hint | String,
rationale | String,
} in
let ontology_check_type = {
decision_string | String,
invariants_at_risk | Array String,
verdict | verdict_type,
} in
let invariant_justification_type = {
invariant | String,
claim | String,
mitigation | String,
} in
let consequences_type = {
positive | Array String,
negative | Array String,
} in
let adr_type = {
id | String | c.AdrIdFormat,
title | String,
status | status_type,
date | String,
context | String,
decision | String,
rationale | Array rationale_entry_type,
consequences | consequences_type,
alternatives_considered | Array alternative_type,
constraints | Array constraint_type | c.NonEmptyConstraints,
ontology_check | ontology_check_type,
related_adrs | Array String | default = [],
supersedes | String | optional,
superseded_by | String | optional,
invariant_justification | invariant_justification_type | optional,
} in
{
AdrStatus = status_type,
Severity = severity_type,
Verdict = verdict_type,
Constraint = constraint_type,
RationaleEntry = rationale_entry_type,
Alternative = alternative_type,
OntologyCheck = ontology_check_type,
InvariantJustification = invariant_justification_type,
Adr = adr_type,
}

1
adrs/constraints.ncl Normal file
View File

@ -0,0 +1 @@
import "adr-constraints.ncl"

1
adrs/defaults.ncl Normal file
View File

@ -0,0 +1 @@
import "adr-defaults.ncl"

317
adrs/reflection.ncl Normal file
View File

@ -0,0 +1,317 @@
let s = import "../reflection/schema.ncl" in
# ADR System — Operational reflection guide
# Covers: authoring, reading, validating, and superseding ADRs in the ontoref ecosystem.
# All content in English. Schema field names (actor, depends_on, verify) are API identifiers.
let AdrAction = [|
'new_adr,
'read_adr,
'validate_decision,
'supersede_adr,
'export_adr,
|] in
{
style = {
format = "Nickel ADR",
schema = "adrs/defaults.ncl",
content_lang = "English",
field_names = "API identifiers — do not translate",
invariant_check = "Every ADR decision string must be validated against .ontology/core.ncl before writing",
},
agent = {
rules = [
"Read adrs/defaults.ncl and adrs/schema.ncl before generating any ADR content",
"Run `nickel export adrs/adr-NNN-*.ncl` to verify the file exports cleanly after writing",
"Check .ontology/core.ncl invariants_at_risk and ontology_check.verdict before marking status = 'Accepted",
"If verdict is 'Risky or 'Unsafe, require explicit justification in InvariantJustification before accepting",
"All string values in id, title, context, decision, rationale, constraints, consequences must be in English",
"Schema field names (name, description, closing_condition) are defined by ontoref-ontology in English",
"Never create an ADR without at least one Hard constraint with a check_hint",
"Superseded ADRs keep status = 'Superseded — do not delete them",
],
},
modes = [
{
id = "new_adr",
trigger = "A significant architectural decision has been made or is being considered",
preconditions = [
"Decision affects the ecosystem architecture, not a single project implementation detail",
"No existing ADR covers this decision",
"adrs/defaults.ncl and adrs/schema.ncl are readable",
],
steps = [
{
id = "check_ontology",
action = 'validate_decision,
actor = 'Agent,
cmd = "nickel export .ontology/core.ncl | get nodes | where level == 'Axiom | get id",
verify = "Output lists the invariant IDs that the decision may affect",
note = "Identify which invariants are at risk before writing the ADR",
},
{
id = "draft_adr",
action = 'new_adr,
depends_on = [{ step = "check_ontology", kind = 'OnSuccess }],
actor = 'Both,
note = "Use the next sequential adr-NNN id. Title must be a declarative statement, not a question.",
},
{
id = "add_constraints",
action = 'new_adr,
depends_on = [{ step = "draft_adr", kind = 'Always }],
actor = 'Both,
note = "Every ADR requires at least one Hard constraint. The check_hint must be an executable command.",
},
{
id = "verify_export",
action = 'export_adr,
depends_on = [{ step = "add_constraints", kind = 'OnSuccess }],
actor = 'Agent,
cmd = "nickel export adrs/adr-NNN-*.ncl",
verify = "Command exits 0 and produces valid JSON",
on_error = { strategy = 'Stop },
},
{
id = "set_accepted",
action = 'new_adr,
depends_on = [{ step = "verify_export", kind = 'OnSuccess }],
actor = 'Human,
note = "Only the project maintainer sets status = 'Accepted. An ADR in 'Proposed is not authoritative.",
},
],
postconditions = [
"adrs/adr-NNN-*.ncl exports cleanly with status = 'Accepted",
"ontology_check.verdict is 'Safe or has explicit InvariantJustification",
"At least one constraint has severity = 'Hard and a non-empty check_hint",
],
},
{
id = "read_as_human",
trigger = "Understanding a past decision or exploring the ADR corpus",
preconditions = [
"adrs/ directory contains at least one .ncl file",
],
steps = [
{
id = "list_adrs",
action = 'read_adr,
actor = 'Human,
cmd = "ls adrs/adr-*.ncl | sort",
verify = "Files are listed in adr-NNN order",
},
{
id = "export_adr",
action = 'export_adr,
depends_on = [{ step = "list_adrs", kind = 'OnSuccess }],
actor = 'Human,
cmd = "nickel export adrs/adr-NNN-*.ncl | jq '{title, status, decision, constraints}'",
verify = "decision and constraints.claim fields are in English",
},
{
id = "check_related",
action = 'read_adr,
depends_on = [{ step = "export_adr", kind = 'Always }],
actor = 'Human,
note = "Follow related_adrs references to understand the decision chain",
},
],
postconditions = [
"Decision chain is understood: context → decision → constraints → ontology_check",
],
},
{
id = "read_as_agent",
trigger = "AI agent needs to understand the current architectural position before proposing changes",
preconditions = [
"Task affects ontoref protocol architecture, crate scope, or schema definitions",
],
steps = [
{
id = "export_all_accepted",
action = 'export_adr,
actor = 'Agent,
cmd = "ls adrs/adr-*.ncl | each { |f| nickel export $f } | where status == 'Accepted",
verify = "Only Accepted ADRs are in the working set",
note = "Proposed and Superseded ADRs are not authoritative — do not use them to justify decisions",
},
{
id = "extract_constraints",
action = 'validate_decision,
depends_on = [{ step = "export_all_accepted", kind = 'OnSuccess }],
actor = 'Agent,
cmd = "nickel export adrs/adr-*.ncl | get constraints | where severity == 'Hard | get check_hint",
verify = "Hard constraints are available as executable check_hints",
},
{
id = "cross_reference_ontology",
action = 'validate_decision,
depends_on = [{ step = "extract_constraints", kind = 'Always }],
actor = 'Agent,
cmd = "nickel export .ontology/core.ncl | get nodes | where invariant == true | get id",
verify = "Invariant IDs match the invariants_at_risk fields across accepted ADRs",
note = "A proposed change that touches any invariant requires a new ADR, not a patch",
},
],
postconditions = [
"Agent has the active constraint set and knows which invariants bound the decision space",
"No change is proposed that violates a Hard constraint without a new ADR",
],
},
{
id = "validate_decision",
trigger = "A proposed code or architecture change needs validation against existing ADRs",
preconditions = [
"The change is described in concrete terms: what is being added, removed, or modified",
"Accepted ADRs are available via `nickel export`",
],
steps = [
{
id = "run_check_hints",
action = 'validate_decision,
actor = 'Agent,
cmd = "nickel export adrs/adr-*.ncl | get constraints | where severity == 'Hard | each { |c| nu -c $c.check_hint }",
verify = "All Hard check_hints exit 0 or produce no output (no violation found)",
on_error = { strategy = 'Stop },
note = "A non-zero exit or match output means a Hard constraint is violated — stop and document",
},
{
id = "check_protocol_invariants",
action = 'validate_decision,
depends_on = [{ step = "run_check_hints", kind = 'OnSuccess }],
actor = 'Agent,
cmd = "nickel export .ontology/core.ncl | get nodes | where invariant == true | get id",
verify = "None of the invariant node IDs are affected by the proposed change",
note = "A change touching any invariant node requires a new ADR, not a patch",
},
{
id = "report_result",
action = 'validate_decision,
depends_on = [{ step = "check_protocol_invariants", kind = 'Always }],
actor = 'Agent,
note = "Report: which constraints passed, which failed, which ADR is implicated for each failure",
},
],
postconditions = [
"All Hard constraints pass or a blocking ADR violation is documented with the implicated constraint ID",
],
},
{
id = "query_constraints",
trigger = "Need to know the current active constraint set, or reconstruct constraints at a specific point in time",
preconditions = [
"adrs/ directory contains exported ADR files",
"`nickel export` is available",
],
steps = [
{
id = "active_constraints",
action = 'read_adr,
actor = 'Agent,
cmd = "ls adrs/adr-*.ncl | each { |f| nickel export $f } | where status == 'Accepted | each { |a| { adr: $a.id, title: $a.title, constraints: $a.constraints } }",
verify = "Output contains only Accepted ADRs with their constraint sets",
note = "This is the authoritative current constraint set. Proposed and Superseded ADRs are excluded.",
},
{
id = "hard_constraints_only",
action = 'validate_decision,
depends_on = [{ step = "active_constraints", kind = 'Always }],
actor = 'Agent,
cmd = "ls adrs/adr-*.ncl | each { |f| nickel export $f } | where status == 'Accepted | get constraints | flatten | where severity == 'Hard | select id claim check_hint",
verify = "Each Hard constraint has a non-empty check_hint",
note = "Hard constraints are non-negotiable. A change that violates one requires a new ADR, not a workaround.",
},
{
id = "point_in_time",
action = 'read_adr,
depends_on = [{ step = "active_constraints", kind = 'Always }],
actor = 'Agent,
cmd = "ls adrs/adr-*.ncl | each { |f| nickel export $f } | where (($it.status == 'Accepted or $it.status == 'Superseded) and $it.date <= \"YYYY-MM\") | where superseded_by? == null | get constraints | flatten",
verify = "Returns the constraint set that was active at the given date",
note = "Replace YYYY-MM with target date. An ADR without superseded_by was still Accepted at that date.",
},
{
id = "supersession_chain",
action = 'read_adr,
depends_on = [{ step = "point_in_time", kind = 'Always }],
actor = 'Agent,
cmd = "ls adrs/adr-*.ncl | each { |f| nickel export $f } | select id status supersedes superseded_by date | sort-by date",
verify = "Each superseded ADR has a superseded_by reference to the replacing ADR",
note = "Follow the chain: superseded_by → new ADR → check its supersedes field to confirm bidirectional link",
},
],
postconditions = [
"Agent has the active Hard constraint set for validation",
"Or: the historical constraint set at a specific date is reconstructed",
"Supersession chain is traceable in both directions via supersedes / superseded_by",
],
},
{
id = "supersede_adr",
trigger = "An existing Accepted ADR must be replaced due to architectural evolution",
preconditions = [
"A new ADR (adr-NNN) has been drafted and exports cleanly",
"The superseding ADR references the old ADR in related_adrs",
"The old ADR ID and title are known",
],
steps = [
{
id = "set_superseded_status",
action = 'supersede_adr,
actor = 'Human,
note = "Edit the old ADR file: set status = 'Superseded. Do not delete the file.",
},
{
id = "verify_new_exports",
action = 'export_adr,
depends_on = [{ step = "set_superseded_status", kind = 'OnSuccess }],
actor = 'Agent,
cmd = "nickel export adrs/adr-NNN-*.ncl",
verify = "New ADR exports with status = 'Accepted",
on_error = { strategy = 'Stop },
},
{
id = "verify_old_exports",
action = 'export_adr,
depends_on = [{ step = "set_superseded_status", kind = 'OnSuccess }],
actor = 'Agent,
cmd = "nickel export adrs/adr-OLD-*.ncl | get status",
verify = "Output is 'Superseded",
on_error = { strategy = 'Stop },
},
{
id = "update_references",
action = 'supersede_adr,
depends_on = [
{ step = "verify_new_exports", kind = 'OnSuccess },
{ step = "verify_old_exports", kind = 'OnSuccess },
],
actor = 'Human,
cmd = "grep -r 'adr-OLD' adrs/ reflection/ .ontology/",
note = "Update any references to the old ADR ID in other files",
},
],
postconditions = [
"Old ADR has status = 'Superseded and remains in adrs/",
"New ADR has status = 'Accepted and references the old ADR in related_adrs",
"No file references the old ADR as if it were still active",
],
},
],
}
| {
modes
| Array (s.Mode AdrAction)
| doc "ADR operational modes — validated against reflection/schema.ncl at eval time",
..
}

1
adrs/schema.ncl Normal file
View File

@ -0,0 +1 @@
import "adr-schema.ncl"

53
install/ontoref-global Executable file
View File

@ -0,0 +1,53 @@
#!/bin/bash
# ontoref — global entry point for the ontoref protocol CLI.
#
# Discovers project root by walking up from CWD looking for .ontology/.
# No per-project wrapper needed. Works from any subdirectory.
#
# Usage:
# ontoref adr l
# ontoref sync
# ONTOREF_PROJECT_ROOT=/path/to/project ontoref adr l # explicit override
#
# Install:
# ln -sf /path/to/ontoref/install/ontoref-global ~/.local/bin/ontoref
set -euo pipefail
ONTOREF_ROOT="${ONTOREF_ROOT:-/Users/Akasha/Development/ontoref}"
if [[ ! -f "${ONTOREF_ROOT}/ontoref" ]]; then
echo "ontoref: entry point not found at ${ONTOREF_ROOT}/ontoref" >&2
echo " Set ONTOREF_ROOT to the correct path." >&2
exit 1
fi
# Walk up from CWD to find the nearest directory containing .ontology/
_find_project_root() {
local dir
dir="$(pwd)"
while [[ "${dir}" != "/" ]]; do
[[ -d "${dir}/.ontology" ]] && echo "${dir}" && return 0
dir="$(dirname "${dir}")"
done
return 1
}
if [[ -z "${ONTOREF_PROJECT_ROOT:-}" ]]; then
if ! ONTOREF_PROJECT_ROOT="$(_find_project_root)"; then
echo "ontoref: no .ontology/ found from $(pwd) up to /" >&2
echo " Run from within a project that has .ontology/, or set ONTOREF_PROJECT_ROOT." >&2
exit 1
fi
fi
export ONTOREF_ROOT
export ONTOREF_PROJECT_ROOT
_paths="${ONTOREF_PROJECT_ROOT}:${ONTOREF_PROJECT_ROOT}/.ontology:${ONTOREF_PROJECT_ROOT}/adrs:${ONTOREF_ROOT}/adrs:${ONTOREF_ROOT}/ontology/schemas:${ONTOREF_ROOT}"
export NICKEL_IMPORT_PATH="${_paths}${NICKEL_IMPORT_PATH:+:${NICKEL_IMPORT_PATH}}"
unset _paths
export ONTOREF_CALLER="${ONTOREF_CALLER:-ontoref}"
exec "${ONTOREF_ROOT}/ontoref" "$@"

View File

@ -0,0 +1,12 @@
# InvariantRequiresAxiom: nodes with invariant=true must have level='Axiom.
# Apply as a post-schema constraint on individual Node records.
std.contract.custom (
fun label =>
fun value =>
if value.invariant && value.level != 'Axiom then
'Error {
message = "Node '%{value.id}': invariant=true requires level='Axiom (got '%{std.string.from value.level})"
}
else
'Ok value
)

View File

@ -0,0 +1,12 @@
# NonEmptyId: ensures the `id` field of any record is a non-empty string.
# Apply as: `data | c.NonEmptyId` before merging with schema.
std.contract.custom (
fun label =>
fun value =>
if !(std.record.has_field "id" value) then
'Error { message = "record must have an 'id' field" }
else if std.string.length value.id == 0 then
'Error { message = "id must not be empty" }
else
'Ok value
)

View File

@ -0,0 +1,9 @@
let s = import "../schemas/core.ncl" in
{
make_node = fun data => s.Node & data,
make_edge = fun data => s.Edge & data,
Node = s.Node,
Edge = s.Edge,
}

View File

@ -0,0 +1,8 @@
let s = import "../schemas/gate.ncl" in
{
make_membrane = fun data => s.Membrane & data,
Membrane = s.Membrane,
OpeningCondition = s.OpeningCondition,
}

View File

@ -0,0 +1,27 @@
let s = import "../schemas/manifest.ncl" in
{
make_manifest = fun data => s.ProjectManifest & data,
make_layer = fun data => s.Layer & data,
make_op_mode = fun data => s.OperationalMode & data,
make_consumption_mode = fun data => s.ConsumptionMode & data,
make_publication_service = fun data => s.PublicationService & data,
make_tool = fun data => s.ToolRequirement & data,
ProjectManifest = s.ProjectManifest,
Layer = s.Layer,
OperationalMode = s.OperationalMode,
ConsumptionMode = s.ConsumptionMode,
PublicationService = s.PublicationService,
ToolRequirement = s.ToolRequirement,
JustfileConvention = s.JustfileConvention,
ClaudeBaseline = s.ClaudeBaseline,
RepoKind = s.RepoKind,
ConsumerType = s.ConsumerType,
ArtifactKind = s.ArtifactKind,
AuditLevel = s.AuditLevel,
AuthMethod = s.AuthMethod,
ServiceScope = s.ServiceScope,
InstallMethod = s.InstallMethod,
JustfileSystem = s.JustfileSystem,
}

View File

@ -0,0 +1,13 @@
let s = import "../schemas/state.ncl" in
{
make_state = fun data => s.State & data,
make_transition = fun data => s.Transition & data,
make_dimension = fun data => s.Dimension & data,
make_coupling = fun data => s.Coupling & data,
State = s.State,
Transition = s.Transition,
Dimension = s.Dimension,
Coupling = s.Coupling,
}

42
ontology/schemas/core.ncl Normal file
View File

@ -0,0 +1,42 @@
let pole_type = [| 'Yang, 'Yin, 'Spiral |] in
let level_type = [| 'Axiom, 'Tension, 'Practice, 'Project, 'Moment |] in
let edge_type = [|
'Contains,
'TensionWith,
'ManifestsIn,
'ValidatedBy,
'FlowsTo,
'CyclesIn,
'SpiralsWith,
'LimitedBy,
'Complements,
|] in
{
Pole = pole_type,
AbstractionLevel = level_type,
EdgeType = edge_type,
Node = {
id | String,
name | String,
pole | pole_type,
level | level_type,
description | String,
invariant | Bool | default = false,
artifact_paths | Array String | default = [],
},
Edge = {
from | String,
to | String,
kind | edge_type,
weight | Number | default = 1,
note | String | default = "",
},
CoreConfig = {
nodes | Array { id | String, name | String, pole | pole_type, level | level_type, description | String, invariant | Bool, artifact_paths | Array String },
edges | Array { from | String, to | String, kind | edge_type, weight | Number, note | String },
},
}

62
ontology/schemas/gate.ncl Normal file
View File

@ -0,0 +1,62 @@
let perm_type = [| 'High, 'Medium, 'Low, 'Closed |] in
let protocol_type = [| 'Observe, 'Absorb, 'Challenge, 'Reject |] in
let duration_type = [| 'Moment, 'Days, 'Weeks, 'Indefinite |] in
let signal_type = [|
'FrameBreakingQuestion,
'UsageFriction,
'ProductiveMisunderstanding,
'HardBug,
'ContextlessInsight,
'SignificantSilence,
'ConnectsToPractice,
'EcosystemRelevance,
'DepthDemonstrated,
'IdentityReinforcement,
'OpportunityAlignment,
|] in
let opening_condition_type = {
max_tension_dimensions | Number,
pending_transitions | Number,
core_stable | Bool,
description | String,
} in
{
Permeability = perm_type,
Protocol = protocol_type,
Duration = duration_type,
SignalType = signal_type,
OpeningCondition = opening_condition_type,
Membrane = {
id | String,
name | String,
description | String,
permeability | perm_type,
accepts | Array signal_type,
protects | Array String,
opening_condition | opening_condition_type,
closing_condition | String,
protocol | protocol_type,
max_duration | duration_type,
active | Bool | default = false,
},
GateConfig = {
membranes | Array {
id | String,
name | String,
description | String,
permeability | perm_type,
accepts | Array signal_type,
protects | Array String,
opening_condition | opening_condition_type,
closing_condition | String,
protocol | protocol_type,
max_duration | duration_type,
active | Bool,
},
},
}

View File

@ -0,0 +1,169 @@
let repo_kind_type = [|
'DevWorkspace,
'PublishedCrate,
'Service,
'Library,
'AgentResource,
'Mixed,
|] in
let consumer_type = [|
'Developer,
'Agent,
'EndUser,
'CI,
'Downstream,
|] in
let artifact_kind_type = [|
'RustDoc,
'JsonSchema,
'ContainerImage,
'CratePackage,
'StaticSite,
'NuPlugin,
'OntologyExport,
|] in
let audit_level_type = [|
'Quick,
'Standard,
'Strict,
|] in
# ── Operational layers ──────────────────────────────────────────────────────
# A layer is a named region of the repo with visibility rules per mode.
# The `committed` flag distinguishes product (true) from process (false).
let layer_type = {
id | String,
paths | Array String,
committed | Bool,
description | String | default = "",
} in
# ── Operational modes ───────────────────────────────────────────────────────
# A mode is an active perspective the developer/agent switches into.
# It determines which layers are visible and what audit level applies.
let op_mode_type = {
id | String,
description | String | default = "",
visible_layers | Array String,
audit_level | audit_level_type | default = 'Standard,
pre_activate | Array String | default = [],
post_activate | Array String | default = [],
} in
# ── Publication service ─────────────────────────────────────────────────────
# Where artifacts go and what operations surround the publish action.
let auth_method_type = [|
'SSH,
'Token,
'OIDC,
'None,
|] in
let service_scope_type = [|
'Public,
'PrivateNetwork,
'LocalRegistry,
'SelfHosted,
|] in
let publication_service_type = {
id | String,
artifact | artifact_kind_type,
scope | service_scope_type,
registry_url | String | default = "",
auth_method | auth_method_type | default = 'None,
pre_publish | Array String | default = [],
post_publish | Array String | default = [],
condition | String | default = "",
trigger | String,
} in
# ── Consumption modes (who consumes, what they need) ────────────────────────
let consumption_mode_type = {
consumer | consumer_type,
needs | Array artifact_kind_type,
audit_level | audit_level_type | default = 'Standard,
description | String | default = "",
} in
# ── Tool requirements ─────────────────────────────────────────────────────
# Declares what tools the project needs. install-tools.nu and sync audit
# consume this to verify availability or trigger installation.
let install_method_type = [|
'Builtin,
'Cargo,
'Npm,
'Brew,
'Pip,
'Manual,
|] in
let tool_requirement_type = {
name | String,
install_method | install_method_type | default = 'Builtin,
version | String | default = "",
required | Bool | default = true,
} in
# ── Justfile convention ──────────────────────────────────────────────────
# Declares expected justfile structure so sync audit can verify completeness.
let justfile_system_type = [| 'Import, 'Mod, 'Hybrid, 'Flat, 'None |] in
let justfile_convention_type = {
system | justfile_system_type | default = 'Mod,
required_modules | Array String | default = ["build", "test", "dev", "ci"],
required_recipes | Array String | default = ["default", "help"],
} in
# ── Claude baseline ─────────────────────────────────────────────────────
# Declares expected .claude/ structure per project.
let claude_baseline_type = {
guidelines | Array String | default = ["bash", "nushell"],
session_hook | Bool | default = true,
stratum_commands | Bool | default = true,
} in
# ── Root manifest ───────────────────────────────────────────────────────────
let manifest_type = {
project | String,
repo_kind | repo_kind_type,
layers | Array layer_type | default = [],
operational_modes | Array op_mode_type | default = [],
consumption_modes | Array consumption_mode_type,
publication_services | Array publication_service_type | default = [],
tools | Array tool_requirement_type | default = [],
justfile | justfile_convention_type | default = {},
claude | claude_baseline_type | default = {},
default_audit | audit_level_type | default = 'Standard,
default_mode | String | default = "dev",
} in
{
RepoKind = repo_kind_type,
ConsumerType = consumer_type,
ArtifactKind = artifact_kind_type,
AuditLevel = audit_level_type,
AuthMethod = auth_method_type,
ServiceScope = service_scope_type,
InstallMethod = install_method_type,
JustfileSystem = justfile_system_type,
Layer = layer_type,
OperationalMode = op_mode_type,
ConsumptionMode = consumption_mode_type,
PublicationService = publication_service_type,
ToolRequirement = tool_requirement_type,
JustfileConvention = justfile_convention_type,
ClaudeBaseline = claude_baseline_type,
ProjectManifest = manifest_type,
}

View File

@ -0,0 +1,59 @@
let tension_type = [| 'High, 'Medium, 'Low, 'Ignored |] in
let horizon_type = [| 'Weeks, 'Months, 'Years, 'Continuous |] in
let state_type = {
id | String,
name | String,
description | String,
tension | tension_type,
} in
let transition_type = {
from | String,
to | String,
condition | String,
catalyst | String,
blocker | String,
horizon | horizon_type,
} in
let coupling_type = {
origin | String,
destination | String,
kind | String,
note | String,
} in
{
TensionLevel = tension_type,
Horizon = horizon_type,
State = state_type,
Transition = transition_type,
Coupling = coupling_type,
Dimension = {
id | String,
name | String,
description | String,
current_state | String,
desired_state | String,
horizon | horizon_type,
states | Array state_type,
transitions | Array transition_type,
coupled_with | Array String | default = [],
},
StateConfig = {
dimensions | Array {
id | String,
name | String,
description | String,
current_state | String,
desired_state | String,
horizon | horizon_type,
states | Array state_type,
transitions | Array transition_type,
coupled_with | Array String,
},
},
}

287
ontoref Executable file
View File

@ -0,0 +1,287 @@
#!/bin/bash
# ontoref — canonical entry point. Shell alias: ontoref
# Release: 0.1.0
# Responsibilities:
# 1. Verify Nushell >= 0.110.0 is installed
# 2. Detect actor (developer | admin | agent | ci)
# 3. Load env vars from .ontoref/config.ncl (NICKEL_IMPORT_PATH etc.)
# 4. Acquire advisory lock for write operations (mkdir-based, cross-platform)
# 5. Delegate to reflection/bin/ontoref.nu
#
# Actor detection priority (highest to lowest):
# --actor <value> explicit flag
# ONTOREF_ACTOR env var pre-set by caller
# CI env vars CI/CD environment
# non-interactive TTY agent / piped context
# default developer
#
# Flags:
# --env-only export env vars and return (for `source onref --env-only`)
# --actor <value> override actor detection
#
# Locking:
# Operations that write shared files acquire a per-resource advisory lock.
# Lock = directory under .ontoref/locks/<resource>.lock (mkdir is POSIX-atomic).
# Stale locks (owner process gone) are cleared automatically.
# Resources: manifest (config apply/rollback), changelog (register), backlog (backlog done/cancel)
set -euo pipefail
_release() { grep "^# Release:" "$0" | sed "s/# Release: //g"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly DISPATCHER="${SCRIPT_DIR}/reflection/bin/ontoref.nu"
readonly CONFIG_NCL="${SCRIPT_DIR}/.ontoref/config.ncl"
readonly LOCKS_DIR="${SCRIPT_DIR}/.ontoref/locks"
# ── Nushell check ─────────────────────────────────────────────────────────────
if ! command -v nu &>/dev/null; then
echo ""
echo " onref requires Nushell (>= 0.110.0)"
echo ""
echo " Install:"
echo " cargo install nu # via Rust"
echo " brew install nushell # macOS"
echo " winget install nushell # Windows"
echo ""
echo " Then re-run: ./ontoref $*"
echo ""
exit 1
fi
# ── Version gate ──────────────────────────────────────────────────────────────
NU_VERSION="$(nu --version 2>/dev/null | tr -d '[:space:]')"
NU_MAJOR="${NU_VERSION%%.*}"
NU_MAJOR="${NU_MAJOR%%[^0-9]*}"
NU_MINOR="${NU_VERSION#*.}"; NU_MINOR="${NU_MINOR%%.*}"
NU_MINOR="${NU_MINOR%%[^0-9]*}"
if [[ "${NU_MAJOR}" -lt 0 ]] || { [[ "${NU_MAJOR}" -eq 0 ]] && [[ "${NU_MINOR}" -lt 110 ]]; }; then
echo ""
echo " Nushell ${NU_VERSION} found — 0.110.0+ required"
echo " Update: cargo install nu --force"
echo ""
exit 1
fi
# ── Version flag (early exit before any env loading) ─────────────────────────
for _arg in "$@"; do
case "${_arg}" in
-V|-v|--version|version)
echo "ontoref $(_release)"
exit 0
;;
esac
done
# ── Actor detection ────────────────────────────────────────────────────────────
# Extract --actor from args without mapfile (macOS Bash 3.2 compatible).
# Sets ACTOR_FROM_ARGS and rebuilds REMAINING_ARGS without the --actor flag.
ACTOR_FROM_ARGS=""
ENV_ONLY=0
REMAINING_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--actor)
shift
ACTOR_FROM_ARGS="${1:-}"
shift
;;
--actor=*)
ACTOR_FROM_ARGS="${1#--actor=}"
shift
;;
--env-only)
ENV_ONLY=1
shift
;;
*)
REMAINING_ARGS+=("$1")
shift
;;
esac
done
# Priority resolution
if [[ -n "${ACTOR_FROM_ARGS}" ]]; then
export ONTOREF_ACTOR="${ACTOR_FROM_ARGS}"
elif [[ -n "${ONTOREF_ACTOR:-}" ]]; then
: # already set by caller
elif [[ -n "${CI:-}" ]] || [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${WOODPECKER_BUILD_NUMBER:-}" ]]; then
export ONTOREF_ACTOR="ci"
elif [[ ! -t 0 ]]; then
export ONTOREF_ACTOR="agent"
else
export ONTOREF_ACTOR="developer"
fi
# ── Load project env vars ──────────────────────────────────────────────────────
load_ontoref_env() {
local fallback_paths="${SCRIPT_DIR}:${SCRIPT_DIR}/.ontology:${SCRIPT_DIR}/adrs:${SCRIPT_DIR}/reflection/requirements"
if [[ ! -f "${CONFIG_NCL}" ]]; then
export NICKEL_IMPORT_PATH="${fallback_paths}"
return 0
fi
local raw_paths=""
if command -v nickel &>/dev/null; then
# shellcheck disable=SC2016 # $env.ONTOREF_ROOT and $p are Nushell variables, not bash
raw_paths="$(nickel export "${CONFIG_NCL}" 2>/dev/null | ONTOREF_ROOT="${SCRIPT_DIR}" nu -c 'from json | get nickel_import_paths | each { |p| $env.ONTOREF_ROOT + "/" + $p } | str join ":"' 2>/dev/null)" || true
fi
if [[ -n "${raw_paths}" ]]; then
export NICKEL_IMPORT_PATH="${raw_paths}"
else
export NICKEL_IMPORT_PATH="${fallback_paths}"
fi
}
if [[ -z "${NICKEL_IMPORT_PATH:-}" ]]; then
load_ontoref_env
fi
# ── Advisory locking (mkdir-based, POSIX-atomic, cross-platform) ──────────────
determine_lock() {
local cmd="${REMAINING_ARGS[0]:-}"
local sub="${REMAINING_ARGS[1]:-}"
case "${cmd}" in
config)
case "${sub}" in
apply|rollback) echo "manifest" ;;
*) echo "" ;;
esac
;;
register) echo "changelog" ;;
backlog)
case "${sub}" in
done|cancel) echo "backlog" ;;
*) echo "" ;;
esac
;;
*) echo "" ;;
esac
}
is_stale_lock() {
local lockdir="$1"
local owner_file="${lockdir}/owner"
if [[ ! -f "${owner_file}" ]]; then
return 0
fi
local owner_pid
owner_pid="$(cut -d: -f1 "${owner_file}" 2>/dev/null)" || return 0
if [[ -z "${owner_pid}" ]]; then return 0; fi
if ! kill -0 "${owner_pid}" 2>/dev/null; then
return 0
fi
return 1
}
ACQUIRED_LOCK=""
acquire_lock() {
local resource="$1"
local timeout="${2:-30}"
local lockdir="${LOCKS_DIR}/${resource}.lock"
local elapsed=0
mkdir -p "${LOCKS_DIR}"
local stale_retries=0
while ! mkdir "${lockdir}" 2>/dev/null; do
local stale=0
# shellcheck disable=SC2310
is_stale_lock "${lockdir}" && stale=1 || true
if [[ "${stale}" -eq 1 ]]; then
stale_retries=$((stale_retries + 1))
if [[ "${stale_retries}" -gt 5 ]]; then
echo " onref: stale lock on '${resource}' could not be cleared after 5 attempts" >&2
return 1
fi
rm -rf "${lockdir}"
sleep 0.1
continue
fi
if [[ "${elapsed}" -ge "${timeout}" ]]; then
local owner="unknown"
[[ -f "${lockdir}/owner" ]] && owner="$(cat "${lockdir}/owner")"
echo " onref: lock timeout on '${resource}' after ${timeout}s (held by: ${owner})" >&2
return 1
fi
sleep 1
elapsed=$(( elapsed + 1 ))
done
local now
now="$(date -u +%Y%m%dT%H%M%SZ)"
echo "$$:${ONTOREF_ACTOR}:${now}" > "${lockdir}/owner"
ACQUIRED_LOCK="${lockdir}"
}
release_lock() {
if [[ -n "${ACQUIRED_LOCK}" ]]; then
rm -rf "${ACQUIRED_LOCK}" 2>/dev/null || true
ACQUIRED_LOCK=""
fi
}
# ── Delegate to Nushell ───────────────────────────────────────────────────────
export ONTOREF_ROOT="${SCRIPT_DIR}"
# Allow callers (consumer project wrappers) to pre-set ONTOREF_PROJECT_ROOT.
# Only default to SCRIPT_DIR when running ontoref against its own repo.
export ONTOREF_PROJECT_ROOT="${ONTOREF_PROJECT_ROOT:-${SCRIPT_DIR}}"
_caller_name="$(basename "$0")"
export ONTOREF_CALLER="./${_caller_name}"
if [[ "${ENV_ONLY}" -eq 1 ]]; then
# shellcheck disable=SC2317
return 0 2>/dev/null || exit 0
fi
# ── Rewrite help flags ────────────────────────────────────────────────────────
# Transform --help, -help, -h anywhere in args into: help <preceding-args>
_has_help=0
_non_help_args=()
for _a in "${REMAINING_ARGS[@]}"; do
case "${_a}" in
--help|-help|-h) _has_help=1 ;;
*) _non_help_args+=("${_a}") ;;
esac
done
if [[ "${_has_help}" -eq 1 ]]; then
if [[ "${#_non_help_args[@]}" -gt 0 ]]; then
REMAINING_ARGS=("help" "${_non_help_args[@]}")
else
REMAINING_ARGS=("help")
fi
fi
# ── Fix trailing flags that require a value ────────────────────────────────
if [[ "${#REMAINING_ARGS[@]}" -gt 0 ]]; then
_last="${REMAINING_ARGS[${#REMAINING_ARGS[@]}-1]}"
# shellcheck disable=SC2249
case "${_last}" in
--fmt|--format|-fmt|-f|--actor|--context|--severity|--backend|--kind|--priority|--status)
REMAINING_ARGS+=("select")
;;
esac
fi
LOCK_RESOURCE="$(determine_lock)"
if [[ -n "${LOCK_RESOURCE}" ]]; then
acquire_lock "${LOCK_RESOURCE}" 30
trap 'release_lock' EXIT INT TERM
nu "${DISPATCHER}" "${REMAINING_ARGS[@]+"${REMAINING_ARGS[@]}"}"
else
nu "${DISPATCHER}" "${REMAINING_ARGS[@]+"${REMAINING_ARGS[@]}"}"
fi

168
reflection/bin/check-prereqs.nu Executable file
View File

@ -0,0 +1,168 @@
#!/usr/bin/env nu
# Check environment prerequisites for ontoref operations.
#
# Usage:
# nu reflection/bin/check-prereqs.nu
# nu reflection/bin/check-prereqs.nu --context ci
# nu reflection/bin/check-prereqs.nu --context agent
# nu reflection/bin/check-prereqs.nu --form new_adr
# nu reflection/bin/check-prereqs.nu --context local_dev --severity Hard
def main [
--context: string = "local_dev",
--form: string = "",
--severity: string = "",
--json,
] {
let base_file = "reflection/requirements/base.ncl"
let contexts_file = "reflection/requirements/contexts.ncl"
# Inline daemon-export with subprocess fallback (standalone script, no module imports).
let export_base = do { ^nickel export $base_file } | complete
if $export_base.exit_code != 0 {
error make { msg: "Failed to load base.ncl — is nickel installed?" }
}
let all_tools = ($export_base.stdout | from json | get tools)
let export_ctx = do { ^nickel export $contexts_file } | complete
if $export_ctx.exit_code != 0 {
error make { msg: "Failed to load contexts.ncl" }
}
let ctx_candidates = ($export_ctx.stdout | from json | get contexts | where id == $context)
if ($ctx_candidates | is-empty) {
error make { msg: $"Unknown context '($context)'. Valid: local_dev | ci | agent | benchmark" }
}
let ctx_def = ($ctx_candidates | first)
let tools = if ($form | is-empty) {
$all_tools | where { |it| $it.contexts | any { |c| $c == $context } }
} else {
$all_tools | where { |it| $it.used_by_forms | any { |f| $f == $form } }
}
let tools = if ($severity | is-empty) {
$tools
} else {
$tools | where { |it| $it.severity == $severity }
}
let results = $tools | each { |tool|
let is_plugin = ($tool.plugin_name? | is-not-empty)
# Presence check: plugin registry or PATH binary
let present = if $is_plugin {
plugin list | where name == $tool.plugin_name | is-not-empty
} else {
(do { run-external "which" $tool.binary } | complete | get exit_code) == 0
}
# Version: run check_cmd via nu -c for both binary and plugin tools
let version = if $present and ($tool.check_cmd | is-not-empty) {
do { nu -c $"($tool.check_cmd) | str trim" } | complete
| if $in.exit_code == 0 { $in.stdout | str trim } else { "unknown" }
} else if $present {
"present"
} else {
"not installed"
}
let version_ok = if $present and $version != "unknown" and $version != "present" {
let min_parts = ($tool.version_min | split row "." | each { |p| $p | into int })
let act_result = (
$version
| parse --regex $tool.version_extract
| if ($in | is-empty) { null } else { $in | first | get capture0 }
)
if ($act_result | is-empty) {
true # Cannot parse version — assume ok (plugin compat versions)
} else {
let act_parts = ($act_result | split row "." | each { |p| $p | into int })
let major_ok = ($act_parts | get 0) >= ($min_parts | get 0)
let minor_ok = if ($act_parts | get 0) == ($min_parts | get 0) {
($act_parts | get 1) >= ($min_parts | get 1)
} else { true }
$major_ok and $minor_ok
}
} else if $present {
true
} else {
false
}
let config_ok = if $present and ($tool.config_check? | is-not-empty) {
do { nu -c $tool.config_check } | complete | get exit_code | $in == 0
} else {
true
}
let display_name = if $is_plugin { $"plugin:($tool.plugin_name)" } else { $tool.binary }
{
id: $tool.id,
binary: $display_name,
severity: $tool.severity,
present: $present,
version: $version,
version_min: $tool.version_min,
version_ok: $version_ok,
config_ok: $config_ok,
status: (if not $present {
"MISSING"
} else if not $version_ok {
"VERSION_LOW"
} else if not $config_ok {
"CONFIG_FAIL"
} else {
"OK"
}),
install_hint: $tool.install_hint,
config_hint: ($tool.config_hint? | default ""),
}
}
if $json {
$results | to json
return
}
print $"Environment check — context: ($context)"
print $"─────────────────────────────────────────────────────"
let fails = $results | where status != "OK"
for r in $results {
let icon = match $r.status {
"OK" => "✓",
"MISSING" => "✗",
"VERSION_LOW" => "↑",
"CONFIG_FAIL" => "⚠",
_ => "?",
}
let sev = if $r.severity == "Hard" { "[Hard]" } else { "[Soft]" }
print $"($icon) ($sev) ($r.binary) ($r.version) [min: ($r.version_min)]"
}
print ""
if ($fails | is-empty) {
print "All checks passed."
return
}
let hard_fails = $fails | where severity == "Hard"
for f in $fails {
print $"── ($f.binary) — ($f.status) ──"
match $f.status {
"MISSING" => { print $" Install: ($f.install_hint)" },
"VERSION_LOW" => { print $" Installed: ($f.version) Required: >= ($f.version_min)" ; print $" Update: ($f.install_hint)" },
"CONFIG_FAIL" => { print $" Config issue: ($f.config_hint)" },
}
print ""
}
if ($hard_fails | is-not-empty) {
let hard_ids = $hard_fails | get binary | str join ", "
error make { msg: $"Hard prerequisites not met: ($hard_ids)" }
}
}

697
reflection/bin/ontoref.nu Executable file
View File

@ -0,0 +1,697 @@
#!/usr/bin/env nu
# ontoref dispatcher — all operations routed from here.
# Invoked by ontoref (alias: onref) after Nushell version is verified and ONTOREF_ACTOR is set.
#
# Business logic lives in reflection/modules/ (domain) and reflection/nulib/ (UI).
# This file only defines `def "main X"` subcommands as thin routing shims.
use ../modules/env.nu *
use ../modules/adr.nu *
use ../modules/forms.nu *
use ../modules/prereqs.nu *
use ../modules/register.nu *
use ../modules/backlog.nu *
use ../modules/config.nu *
use ../modules/sync.nu *
use ../modules/coder.nu *
use ../modules/manifest.nu *
use ../modules/describe.nu *
use ../modules/store.nu *
use ../modules/services.nu *
use ../modules/nats.nu *
use ../nulib/fmt.nu *
use ../nulib/shared.nu *
use ../nulib/help.nu [help-group]
use ../nulib/interactive.nu [missing-target, run-interactive]
use ../nulib/dashboard.nu [run-overview, run-health, run-status]
use ../nulib/modes.nu [list-modes, show-mode, run-modes-interactive, run-mode]
use ../nulib/logger.nu [log-action, log-record, log-show-config, log-query, log-follow]
# ── Helpers ───────────────────────────────────────────────────────────────────
def pick-mode []: nothing -> string {
let modes = (list-modes)
if ($modes | is-empty) { print " No modes found."; return "" }
# Check if stdin is a TTY via external test command.
let is_tty = (do { ^test -t 0 } | complete | get exit_code) == 0
if not $is_tty {
print ""
for m in $modes {
print $" ($m.id) (ansi dark_gray)($m.trigger)(ansi reset)"
}
print ""
print $" (ansi dark_gray)Usage: onref run <mode-id>(ansi reset)"
return ""
}
let ids = ($modes | each { |m| $"($m.id) (ansi dark_gray)($m.trigger)(ansi reset)" })
let picked = ($ids | input list $"(ansi cyan_bold)Run mode:(ansi reset) ")
if ($picked | is-empty) { return "" }
$picked | split row " " | first
}
# ── Entry ─────────────────────────────────────────────────────────────────────
def "main" [shortcut?: string] {
match ($shortcut | default "") {
"ru" => { main run },
"f" => { print "Usage: onref f <term>"; },
"h" | "help" | "-help" | "--help" => { main help },
"" => { show-usage-brief },
_ => { print $"Unknown command: ($shortcut). Run: onref help" },
}
}
def show-usage-brief [] {
let caller = ($env.ONTOREF_CALLER? | default "onref")
print $"\nUsage: ($caller) [command] [options]\n"
print $"Use '($caller) help' for available commands\n"
}
def "main help" [group?: string] {
if ($group | is-not-empty) {
help-group $group
return
}
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let cmd = ($env.ONTOREF_CALLER? | default "./onref")
let brief = adrs-brief
let adr_status = $"($brief.accepted)A/($brief.superseded)S/($brief.proposed)P"
print ""
fmt-header "ontoref — ecosystem patterns and tooling"
fmt-sep
print $"(ansi white_bold)Actor(ansi reset): (ansi cyan)($actor)(ansi reset) | (ansi white_bold)Root(ansi reset): (ansi cyan)($env.ONTOREF_ROOT)(ansi reset)"
print ""
fmt-section "COMMAND GROUPS"
print ""
fmt-cmd $"($cmd) help check" "prerequisites and environment checks"
fmt-cmd $"($cmd) help form" "interactive forms (new_adr, register, etc.)"
fmt-cmd $"($cmd) help mode" "operational modes (inspect + execute)"
fmt-cmd $"($cmd) help adr" $"ADR management (fmt-badge $"($adr_status)")"
fmt-cmd $"($cmd) help register" "record changes → CHANGELOG + ADR + ontology"
fmt-cmd $"($cmd) help backlog" "roadmap, items, promotions"
fmt-cmd $"($cmd) help config" "sealed config profiles, drift, rollback"
fmt-cmd $"($cmd) help sync" "ontology↔code sync, drift detection, proposals"
fmt-cmd $"($cmd) help coder" ".coder/ process memory: record, log, triage, publish"
fmt-cmd $"($cmd) help manifest" "operational modes, publication services, layers"
fmt-cmd $"($cmd) help describe" "project self-knowledge: what, how, why, impact"
fmt-cmd $"($cmd) help log" "action audit trail, follow, filter"
print ""
fmt-section "QUICK REFERENCE"
print ""
fmt-cmd $"($cmd) init" "run actor-configured init mode (from actor_init in config)"
fmt-cmd $"($cmd) run <mode-id>" "execute a mode (shortcut for mode run)"
fmt-cmd $"($cmd) find <term>" "search ontology: selector, detail, connections, usage"
fmt-cmd $"($cmd) about" "project identity and summary"
fmt-cmd $"($cmd) diagram" "terminal box diagram of project architecture"
fmt-cmd $"($cmd) overview" "single-screen project snapshot: identity, crates, health"
fmt-cmd $"($cmd) health" "quick health bar (use --full for deep audit)"
fmt-cmd $"($cmd) status" "project dashboard: health, state, recent activity"
fmt-cmd $"($cmd) nats" "NATS event system (status, listen, emit)"
fmt-cmd $"($cmd) services" "daemon lifecycle (start, stop, restart, status, health)"
fmt-cmd $"($cmd) check" "run prerequisite checks"
fmt-cmd $"($cmd) adr list" "list ADRs with status"
fmt-cmd $"($cmd) constraint" "active Hard constraints"
fmt-cmd $"($cmd) backlog roadmap" "state dimensions + open items"
fmt-cmd $"($cmd) config audit" "verify all profiles"
fmt-cmd $"($cmd) log --tail 10 -t" "last 10 actions with timestamps"
print ""
fmt-section "ALIASES"
print ""
print $" (ansi cyan)ad(ansi reset) → adr (ansi cyan)d(ansi reset) → describe (ansi cyan)ck(ansi reset) → check (ansi cyan)con(ansi reset) → constraint"
print $" (ansi cyan)rg(ansi reset) → register (ansi cyan)bkl(ansi reset) → backlog (ansi cyan)cfg(ansi reset) → config (ansi cyan)cod(ansi reset) → coder"
print $" (ansi cyan)mf(ansi reset) → manifest (ansi cyan)dg(ansi reset) → diagram (ansi cyan)md(ansi reset) → mode (ansi cyan)st(ansi reset) → status"
print $" (ansi cyan)fm(ansi reset) → form (ansi cyan)f(ansi reset) → find (ansi cyan)ru(ansi reset) → run \(mode\) (ansi cyan)sv(ansi reset) → services"
print $" (ansi cyan)nv(ansi reset) → nats"
print ""
print $" (ansi dark_gray)Tip: any group accepts(ansi reset) (ansi cyan)h(ansi reset) (ansi dark_gray)for help,(ansi reset) (ansi cyan)?(ansi reset) (ansi dark_gray)for interactive selector, or bare for picker(ansi reset)"
print ""
}
# ── Check / Forms ─────────────────────────────────────────────────────────────
def "main check" [--context: string = "", --form: string = "", --severity: string = "", --json] {
log-action "check" "read"
if $json {
prereqs check --context $context --form $form --severity $severity --json
} else {
prereqs check --context $context --form $form --severity $severity
}
}
def "main form" [action?: string] { missing-target "form" $action }
def "main form help" [] { help-group "form" }
def "main form run" [name: string, --backend: string = "cli"] {
log-action $"form run ($name)" "interactive"
form run $name --backend $backend
}
def "main form list" [] {
log-action "form list" "read"
print ""
for f in (forms list) {
print $" ($f.name)"
print $" ($f.description)"
print $" Agent: nickel-export reflection/forms/($f.name).ncl | get elements | where type != \"section_header\""
print ""
}
}
def "main form ls" [] { log-action "form list" "read"; forms list }
# ── Modes ─────────────────────────────────────────────────────────────────────
def "main mode" [action?: string] { missing-target "mode" $action }
def "main mode help" [] { help-group "mode" }
def "main mode list" [--fmt (-f): string = ""] {
let modes = (list-modes)
log-action "mode list" "read"
let f = (resolve-fmt $fmt [text table json yaml toml])
match $f {
"json" => { print ($modes | to json) },
"yaml" => { print ($modes | to yaml) },
"toml" => { print ({ modes: $modes } | to toml) },
"table" => { print ($modes | table --expand) },
_ => {
print ""
for m in $modes {
print $" ($m.id)"
print $" Trigger: ($m.trigger)"
if $m.steps > 0 { print $" Steps: ($m.steps)" }
print ""
}
},
}
}
def "main mode select" [] {
let modes = (list-modes)
run-modes-interactive $modes
}
def "main mode show" [id: string, --fmt (-f): string = ""] {
log-action $"mode show ($id)" "read"
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let f = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "md" }
show-mode $id $f
}
def "main mode run" [id: string, --dry-run (-n), --yes (-y)] {
log-action $"mode run ($id)" "write"
run-mode $id --dry-run=$dry_run --yes=$yes
}
# ── ADR ───────────────────────────────────────────────────────────────────────
def "main adr" [action?: string] { missing-target "adr" $action }
def "main adr help" [] { help-group "adr" }
def "main adr list" [--fmt (-f): string = ""] {
log-action "adr list" "read"
let f = (resolve-fmt $fmt [table md json yaml toml])
adr list --fmt $f
}
def "main adr validate" [] { log-action "adr validate" "read"; adr validate }
def "main adr accept" [id: string] { log-action $"adr accept ($id)" "write"; adr accept $id }
def "main adr show" [id?: string, --interactive (-i), --fmt (-f): string = ""] {
log-action $"adr show ($id | default '?')" "read"
let f = (resolve-fmt $fmt [md table json yaml toml])
if $interactive { adr show --interactive --fmt $f } else { adr show $id --fmt $f }
}
def "main adr l" [--fmt (-f): string = ""] { main adr list --fmt $fmt }
def "main adr v" [] { main adr validate }
def "main adr s" [id?: string, --fmt (-f): string = ""] { main adr show $id --fmt $fmt }
# ── Constraints / Register ───────────────────────────────────────────────────
def "main constraint" [--fmt (-f): string = ""] {
log-action "constraint" "read"
let f = (resolve-fmt $fmt [table md json yaml toml])
constraints --fmt $f
}
def "main register" [--backend: string = "cli"] { log-action "register" "write"; register run --backend $backend }
# ── Backlog ───────────────────────────────────────────────────────────────────
def "main backlog" [action?: string] { missing-target "backlog" $action }
def "main backlog help" [] { help-group "backlog" }
def "main backlog roadmap" [] { log-action "backlog roadmap" "read"; backlog roadmap }
def "main backlog list" [--status: string = "", --kind: string = "", --fmt (-f): string = ""] {
log-action "backlog list" "read"
let f = (resolve-fmt $fmt [table md json yaml toml])
backlog list --status $status --kind $kind --fmt $f
}
def "main backlog show" [id: string] { log-action $"backlog show ($id)" "read"; backlog show $id }
def "main backlog add" [title: string, --kind: string = "Todo", --priority: string = "Medium", --detail: string = "", --dim: string = "", --adr: string = "", --mode: string = ""] {
log-action $"backlog add ($title)" "write"
backlog add $title --kind $kind --priority $priority --detail $detail --dim $dim --adr $adr --mode $mode
}
def "main backlog done" [id: string] { log-action $"backlog done ($id)" "write"; backlog done $id }
def "main backlog cancel" [id: string] { log-action $"backlog cancel ($id)" "write"; backlog cancel $id }
def "main backlog promote" [id: string] { log-action $"backlog promote ($id)" "write"; backlog promote $id }
# ── Config ────────────────────────────────────────────────────────────────────
def "main config" [action?: string] { missing-target "config" $action }
def "main config help" [] { help-group "config" }
def "main config show" [profile: string, --fmt (-f): string = ""] {
log-action $"config show ($profile)" "read"
let f = (resolve-fmt $fmt [table json yaml toml])
config show $profile --fmt $f
}
def "main config history" [profile: string, --fmt (-f): string = ""] {
log-action $"config history ($profile)" "read"
let f = (resolve-fmt $fmt [table json yaml toml])
config history $profile --fmt $f
}
def "main config diff" [profile: string, from_id: string, to_id: string] { log-action $"config diff ($profile)" "read"; config diff $profile $from_id $to_id }
def "main config verify" [profile: string] { log-action $"config verify ($profile)" "read"; config verify $profile }
def "main config audit" [] { log-action "config audit" "read"; config audit }
def "main config apply" [profile: string, --adr: string = "", --pr: string = "", --bug: string = "", --note: string = ""] {
log-action $"config apply ($profile)" "write"
config apply $profile --adr $adr --pr $pr --bug $bug --note $note
}
def "main config rollback" [profile: string, to_id: string, --adr: string = "", --note: string = ""] {
log-action $"config rollback ($profile) ($to_id)" "write"
config rollback $profile $to_id --adr $adr --note $note
}
# ── Sync ──────────────────────────────────────────────────────────────────────
def "main sync" [action?: string] { missing-target "sync" $action }
def "main sync help" [] { help-group "sync" }
def "main sync scan" [--level: string = "auto"] { log-action "sync scan" "read"; sync scan --level $level }
def "main sync diff" [--quick] { log-action "sync diff" "read"; if $quick { sync diff --quick } else { sync diff } }
def "main sync propose" [] { log-action "sync propose" "read"; sync propose }
def "main sync apply" [] { log-action "sync apply" "write"; sync apply }
def "main sync state" [] { log-action "sync state" "read"; sync state }
def "main sync audit" [--fmt (-f): string = "", --strict, --quick] {
log-action "sync audit" "read"
let f = (resolve-fmt $fmt [text json])
sync audit --fmt $f --strict=$strict --quick=$quick
}
def "main sync watch" [] { log-action "sync watch" "read"; sync watch }
# ── Coder ─────────────────────────────────────────────────────────────────────
def "main coder" [action?: string] { missing-target "coder" $action }
def "main coder help" [] { help-group "coder" }
def "main coder authors" [] { log-action "coder authors" "read"; coder authors }
def "main coder init" [author: string, --actor: string = "Human", --model: string = ""] {
log-action $"coder init ($author)" "write"
coder init $author --actor $actor --model $model
}
def "main coder record" [
author: string, content: string,
--kind (-k): string = "info", --category (-c): string = "", --title (-t): string,
--tags: list<string> = [], --relates_to: list<string> = [],
--trigger: string = "", --files_touched: list<string> = [],
--domain (-d): string = "", --reusable (-r),
] {
log-action $"coder record ($author) ($title)" "write"
coder record $author $content --kind $kind --category $category --title $title --tags $tags --relates_to $relates_to --trigger $trigger --files_touched $files_touched --domain $domain --reusable=$reusable
}
def "main coder log" [--author (-a): string = "", --category (-c): string = "", --tag (-t): string = "", --kind (-k): string = "", --domain (-d): string = "", --limit (-l): int = 0] {
log-action "coder log" "read"
coder log --author $author --category $category --tag $tag --kind $kind --domain $domain --limit $limit
}
def "main coder export" [--author (-a): string = "", --category (-c): string = "", --format (-f): string = "json"] {
log-action "coder export" "read"
let f = (resolve-fmt $format [json jsonl csv])
coder export --author $author --category $category --format $f
}
def "main coder triage" [author: string, --dry-run (-n), --interactive (-i)] {
log-action $"coder triage ($author)" "write"
coder triage $author --dry-run=$dry_run --interactive=$interactive
}
def "main coder ls" [author: string = "", --category (-c): string = ""] { log-action "coder ls" "read"; coder ls $author --category $category }
def "main coder search" [pattern: string, --author (-a): string = ""] { log-action $"coder search ($pattern)" "read"; coder search $pattern --author $author }
def "main coder publish" [author: string, category: string, --dry-run (-n), --all (-a)] {
log-action $"coder publish ($author) ($category)" "write"
coder publish $author $category --dry-run=$dry_run --all=$all
}
def "main coder graduate" [source_category: string, --target (-t): string = "reflection/knowledge", --dry-run (-n)] {
log-action $"coder graduate ($source_category)" "write"
coder graduate $source_category --target $target --dry-run=$dry_run
}
# ── Manifest ──────────────────────────────────────────────────────────────────
def "main manifest" [action?: string] { missing-target "manifest" $action }
def "main manifest help" [] { help-group "manifest" }
def "main manifest mode" [id: string, --dry-run (-n)] { log-action $"manifest mode ($id)" "read"; manifest mode $id --dry-run=$dry_run }
def "main manifest mode list" [--fmt (-f): string = "table"] {
log-action "manifest mode list" "read"
let f = (resolve-fmt $fmt [table json yaml toml])
manifest mode list --fmt $f
}
def "main manifest publish" [id: string, --dry-run (-n), --yes (-y)] {
log-action $"manifest publish ($id)" "write"
manifest publish $id --dry-run=$dry_run --yes=$yes
}
def "main manifest publish list" [--fmt (-f): string = "table"] {
log-action "manifest publish list" "read"
let f = (resolve-fmt $fmt [table json yaml toml])
manifest publish list --fmt $f
}
def "main manifest layers" [--mode (-m): string = ""] { log-action "manifest layers" "read"; manifest layers --mode $mode }
def "main manifest consumers" [--fmt (-f): string = "table"] {
log-action "manifest consumers" "read"
let f = (resolve-fmt $fmt [table json yaml toml])
manifest consumers --fmt $f
}
# ── Describe ──────────────────────────────────────────────────────────────────
def "main describe" [action?: string] { missing-target "describe" $action }
def "main describe help" [] { help-group "describe" }
def "main describe project" [--fmt (-f): string = "", --actor: string = ""] {
log-action "describe project" "read"
let f = (resolve-fmt $fmt [text table json yaml toml]); describe project --fmt $f --actor $actor
}
def "main describe capabilities" [--fmt (-f): string = "", --actor: string = ""] {
log-action "describe capabilities" "read"
let f = (resolve-fmt $fmt [text table json yaml toml]); describe capabilities --fmt $f --actor $actor
}
def "main describe constraints" [--fmt (-f): string = "", --actor: string = ""] {
log-action "describe constraints" "read"
let f = (resolve-fmt $fmt [text table json yaml toml]); describe constraints --fmt $f --actor $actor
}
def "main describe tools" [--fmt (-f): string = "", --actor: string = ""] {
log-action "describe tools" "read"
let f = (resolve-fmt $fmt [text table json yaml toml]); describe tools --fmt $f --actor $actor
}
def "main describe impact" [node_id: string, --depth: int = 2, --fmt (-f): string = ""] {
log-action $"describe impact ($node_id)" "read"
let f = (resolve-fmt $fmt [text table json yaml toml]); describe impact $node_id --depth $depth --fmt $f
}
def "main describe why" [id: string, --fmt (-f): string = ""] {
log-action $"describe why ($id)" "read"
let f = (resolve-fmt $fmt [text table json yaml toml]); describe why $id --fmt $f
}
def "main describe find" [term: string, --level: string = "", --fmt (-f): string = ""] {
log-action $"describe find ($term)" "read"
describe find $term --level $level --fmt $fmt
}
def "main describe features" [id?: string, --fmt (-f): string = "", --actor: string = ""] {
log-action $"describe features ($id | default '')" "read"
let f = (resolve-fmt $fmt [text table json yaml toml])
if ($id | is-empty) or ($id == "") { describe features --fmt $f --actor $actor } else { describe features $id --fmt $f --actor $actor }
}
def "main describe connections" [--fmt (-f): string = "", --actor: string = ""] {
log-action "describe connections" "read"
let f = (resolve-fmt $fmt [text json])
describe connections --fmt $f --actor $actor
}
# ── Diagram ───────────────────────────────────────────────────────────────────
def "main diagram" [] {
log-action "diagram" "read"
let root = (project-root)
let project_diagram = $"($root)/assets/main-diagram.md"
let onref_diagram = $"($env.ONTOREF_ROOT)/assets/main-diagram.md"
let diagram_file = if ($project_diagram | path exists) {
$project_diagram
} else if ($onref_diagram | path exists) {
$onref_diagram
} else {
print " Diagram not found"
return
}
let content = (open $diagram_file --raw)
let stripped = ($content | lines | where { |l| not ($l | str starts-with "```") } | str join "\n")
print $stripped
}
# ── About ─────────────────────────────────────────────────────────────────────
def "main about" [] {
log-action "about" "read"
let root = (project-root)
let name = ($root | path basename)
let ascii_file = $"($root)/assets/($name)_ascii.txt"
if ($ascii_file | path exists) {
let ascii = (open $ascii_file --raw)
print ""
print $ascii
}
let project_about = $"($root)/assets/about.md"
let onref_about = $"($env.ONTOREF_ROOT)/assets/about.md"
let about_file = if ($project_about | path exists) {
$project_about
} else if ($onref_about | path exists) {
$onref_about
} else {
print " About not found"
return
}
let content = (open $about_file --raw)
print $content
}
# ── Overview / Health / Status ────────────────────────────────────────────────
def "main overview" [--fmt (-f): string = ""] {
log-action "overview" "read"
let f = (resolve-fmt $fmt [text table json yaml toml])
run-overview $f
}
def "main health" [--fmt (-f): string = "", --full] {
log-action "health" "read"
let f = (resolve-fmt $fmt [text table json yaml toml])
run-health $f $full
}
def "main status" [--fmt (-f): string = ""] {
log-action "status" "read"
let f = (resolve-fmt $fmt [text table json yaml toml])
run-status $f
}
# ── Log ──────────────────────────────────────────────────────────────────────
def "main log" [
action?: string,
--follow (-f),
--latest (-l),
--since: string = "",
--until: string = "",
--tail: int = -1,
--timestamps (-t),
--level: string = "",
--actor: string = "",
--query (-q): list<string> = [],
--fmt: string = "text",
] {
let act = ($action | default "")
if $act == "h" or $act == "help" {
help-group "log"
return
}
if $act == "config" {
log-show-config
return
}
if $follow {
log-follow --timestamps=$timestamps --query $query
return
}
log-query --tail_n $tail --since $since --until $until --latest=$latest --timestamps=$timestamps --level $level --actor $actor --query $query --fmt $fmt
}
def "main log record" [
action: string,
--level (-l): string = "write",
--author (-a): string = "",
--actor: string = "",
] {
log-record $action --level $level --author $author --actor $actor
}
# ── Aliases ───────────────────────────────────────────────────────────────────
# All aliases delegate to canonical commands → single log-action call site.
# ad=adr, d=describe, ck=check, con=constraint, rg=register, f=find, ru=run,
# bkl=backlog, cfg=config, cod=coder, mf=manifest, dg=diagram, md=mode, fm=form, st=status, h=help
def "main ad" [action?: string] { main adr $action }
def "main ad help" [] { help-group "adr" }
def "main ad list" [--fmt (-f): string = ""] { main adr list --fmt $fmt }
def "main ad l" [--fmt (-f): string = ""] { main adr list --fmt $fmt }
def "main ad validate" [] { main adr validate }
def "main ad accept" [id: string] { main adr accept $id }
def "main ad a" [id: string] { main adr accept $id }
def "main ad show" [id?: string, --interactive (-i), --fmt (-f): string = ""] { main adr show $id --interactive=$interactive --fmt $fmt }
def "main ad s" [id?: string, --interactive (-i), --fmt (-f): string = ""] { main adr show $id --interactive=$interactive --fmt $fmt }
def "main ck" [--context: string = "", --form: string = "", --severity: string = "", --json] { main check --context $context --form $form --severity $severity --json=$json }
def "main con" [--fmt (-f): string = ""] { main constraint --fmt $fmt }
def "main rg" [--backend: string = "cli"] { main register --backend $backend }
def "main d" [action?: string] { main describe $action }
def "main d help" [] { help-group "describe" }
def "main d project" [--fmt (-f): string = "", --actor: string = ""] { main describe project --fmt $fmt --actor $actor }
def "main d p" [--fmt (-f): string = "", --actor: string = ""] { main describe project --fmt $fmt --actor $actor }
def "main d capabilities" [--fmt (-f): string = "", --actor: string = ""] { main describe capabilities --fmt $fmt --actor $actor }
def "main d cap" [--fmt (-f): string = "", --actor: string = ""] { main describe capabilities --fmt $fmt --actor $actor }
def "main d constraints" [--fmt (-f): string = "", --actor: string = ""] { main describe constraints --fmt $fmt --actor $actor }
def "main d con" [--fmt (-f): string = "", --actor: string = ""] { main describe constraints --fmt $fmt --actor $actor }
def "main d tools" [--fmt (-f): string = "", --actor: string = ""] { main describe tools --fmt $fmt --actor $actor }
def "main d t" [--fmt (-f): string = "", --actor: string = ""] { main describe tools --fmt $fmt --actor $actor }
def "main d tls" [--fmt (-f): string = "", --actor: string = ""] { main describe tools --fmt $fmt --actor $actor }
def "main d find" [term: string, --level: string = "", --fmt (-f): string = ""] { main describe find $term --level $level --fmt $fmt }
def "main d fi" [term: string, --level: string = "", --fmt (-f): string = ""] { main describe find $term --level $level --fmt $fmt }
def "main d features" [id?: string, --fmt (-f): string = "", --actor: string = ""] { main describe features $id --fmt $fmt --actor $actor }
def "main d fea" [id?: string, --fmt (-f): string = "", --actor: string = ""] { main describe features $id --fmt $fmt --actor $actor }
def "main d f" [id?: string, --fmt (-f): string = "", --actor: string = ""] { main describe features $id --fmt $fmt --actor $actor }
def "main d impact" [node_id: string, --depth: int = 2, --fmt (-f): string = ""] { main describe impact $node_id --depth $depth --fmt $fmt }
def "main d i" [node_id: string, --depth: int = 2, --fmt (-f): string = ""] { main describe impact $node_id --depth $depth --fmt $fmt }
def "main d imp" [node_id: string, --depth: int = 2, --fmt (-f): string = ""] { main describe impact $node_id --depth $depth --fmt $fmt }
def "main d why" [id: string, --fmt (-f): string = ""] { main describe why $id --fmt $fmt }
def "main d w" [id: string, --fmt (-f): string = ""] { main describe why $id --fmt $fmt }
def "main d connections" [--fmt (-f): string = "", --actor: string = ""] { main describe connections --fmt $fmt --actor $actor }
def "main d conn" [--fmt (-f): string = "", --actor: string = ""] { main describe connections --fmt $fmt --actor $actor }
def "main bkl" [action?: string] { main backlog $action }
def "main bkl help" [] { help-group "backlog" }
def "main bkl roadmap" [] { main backlog roadmap }
def "main bkl r" [] { main backlog roadmap }
def "main bkl list" [--status: string = "", --kind: string = "", --fmt (-f): string = ""] { main backlog list --status $status --kind $kind --fmt $fmt }
def "main bkl l" [--status: string = "", --kind: string = "", --fmt (-f): string = ""] { main backlog list --status $status --kind $kind --fmt $fmt }
def "main bkl show" [id: string] { main backlog show $id }
def "main bkl add" [title: string, --kind: string = "Todo", --priority: string = "Medium", --detail: string = "", --dim: string = "", --adr: string = "", --mode: string = ""] {
main backlog add $title --kind $kind --priority $priority --detail $detail --dim $dim --adr $adr --mode $mode
}
def "main bkl done" [id: string] { main backlog done $id }
def "main bkl cancel" [id: string] { main backlog cancel $id }
def "main bkl promote" [id: string] { main backlog promote $id }
def "main bkl p" [id: string] { main backlog promote $id }
def "main cfg" [action?: string] { main config $action }
def "main cfg help" [] { help-group "config" }
def "main cfg show" [profile: string, --fmt (-f): string = ""] { main config show $profile --fmt $fmt }
def "main cfg history" [profile: string, --fmt (-f): string = ""] { main config history $profile --fmt $fmt }
def "main cfg diff" [profile: string, from_id: string, to_id: string] { main config diff $profile $from_id $to_id }
def "main cfg verify" [profile: string] { main config verify $profile }
def "main cfg audit" [] { main config audit }
def "main cfg apply" [profile: string, --adr: string = "", --pr: string = "", --bug: string = "", --note: string = ""] { main config apply $profile --adr $adr --pr $pr --bug $bug --note $note }
def "main cfg rollback" [profile: string, to_id: string, --adr: string = "", --note: string = ""] { main config rollback $profile $to_id --adr $adr --note $note }
def "main cod" [action?: string] { main coder $action }
def "main cod help" [] { help-group "coder" }
def "main cod authors" [] { main coder authors }
def "main cod init" [author: string, --actor: string = "Human", --model: string = ""] { main coder init $author --actor $actor --model $model }
def "main cod record" [
author: string, content: string,
--kind (-k): string = "info", --category (-c): string = "", --title (-t): string,
--tags: list<string> = [], --relates_to: list<string> = [],
--trigger: string = "", --files_touched: list<string> = [],
--domain (-d): string = "", --reusable (-r),
] { main coder record $author $content --kind $kind --category $category --title $title --tags $tags --relates_to $relates_to --trigger $trigger --files_touched $files_touched --domain $domain --reusable=$reusable }
def "main cod log" [--author (-a): string = "", --category (-c): string = "", --tag (-t): string = "", --kind (-k): string = "", --domain (-d): string = "", --limit (-l): int = 0] {
main coder log --author $author --category $category --tag $tag --kind $kind --domain $domain --limit $limit
}
def "main cod export" [--author (-a): string = "", --category (-c): string = "", --format (-f): string = "json"] { main coder export --author $author --category $category --format $format }
def "main cod triage" [author: string, --dry-run (-n), --interactive (-i)] { main coder triage $author --dry-run=$dry_run --interactive=$interactive }
def "main cod ls" [author: string = "", --category (-c): string = ""] { main coder ls $author --category $category }
def "main cod search" [pattern: string, --author (-a): string = ""] { main coder search $pattern --author $author }
def "main cod publish" [author: string, category: string, --dry-run (-n), --all (-a)] { main coder publish $author $category --dry-run=$dry_run --all=$all }
def "main cod graduate" [source_category: string, --target (-t): string = "reflection/knowledge", --dry-run (-n)] { main coder graduate $source_category --target $target --dry-run=$dry_run }
def "main mf" [action?: string] { main manifest $action }
def "main mf help" [] { help-group "manifest" }
def "main mf mode" [id: string, --dry-run (-n)] { main manifest mode $id --dry-run=$dry_run }
def "main mf mode list" [--fmt (-f): string = "table"] { main manifest mode list --fmt $fmt }
def "main mf publish" [id: string, --dry-run (-n), --yes (-y)] { main manifest publish $id --dry-run=$dry_run --yes=$yes }
def "main mf publish list" [--fmt (-f): string = "table"] { main manifest publish list --fmt $fmt }
def "main mf layers" [--mode (-m): string = ""] { main manifest layers --mode $mode }
def "main mf consumers" [--fmt (-f): string = "table"] { main manifest consumers --fmt $fmt }
def "main md" [action?: string] { main mode $action }
def "main md help" [] { main mode help }
def "main md list" [--fmt (-f): string = ""] { main mode list --fmt $fmt }
def "main md l" [--fmt (-f): string = ""] { main mode list --fmt $fmt }
def "main md show" [id: string, --fmt (-f): string = ""] { main mode show $id --fmt $fmt }
def "main md s" [id: string, --fmt (-f): string = ""] { main mode show $id --fmt $fmt }
def "main md run" [id: string, --dry-run (-n), --yes (-y)] { main mode run $id --dry-run=$dry_run --yes=$yes }
def "main md select" [] { main mode select }
def "main fm" [action?: string] { main form $action }
def "main fm help" [] { main form help }
def "main fm list" [] { main form list }
def "main fm l" [] { main form list }
def "main fm run" [name: string, --backend: string = "cli"] { main form run $name --backend $backend }
def "main st" [--fmt (-f): string = ""] { main status --fmt $fmt }
def "main run" [id?: string, --dry-run (-n), --yes (-y)] {
let act = ($id | default "")
if $act == "" or $act == "?" or $act == "select" {
let mode_id = (pick-mode)
if ($mode_id | is-empty) { return }
main mode run $mode_id --dry-run=$dry_run --yes=$yes
return
}
if $act == "l" or $act == "list" { main mode list; return }
if $act == "h" or $act == "help" { help-group "mode"; return }
main mode run $act --dry-run=$dry_run --yes=$yes
}
def "main ru" [id?: string, --dry-run (-n), --yes (-y)] { main run $id --dry-run=$dry_run --yes=$yes }
def "main find" [term: string, --level: string = "", --fmt (-f): string = ""] { main describe find $term --level $level --fmt $fmt }
def "main f" [term: string, --level: string = "", --fmt (-f): string = ""] { main describe find $term --level $level --fmt $fmt }
def "main dg" [] { main diagram }
def "main h" [group?: string] { main help $group }
# ── NATS Events ───────────────────────────────────────────────────────────────
def "main nats" [action?: string]: nothing -> nothing {
match ($action | default "") {
"" => { nats-status },
"status" => { nats-status },
"listen" => { nats-listen },
"emit" => { print "Usage: onref nats emit <event> [--project X]"; },
_ => { print $"Unknown action: ($action). Use: status, listen, emit" }
}
}
def "main nv" [action?: string]: nothing -> nothing { main nats $action }
# ── Services ──────────────────────────────────────────────────────────────────
def "main services" [action?: string, id?: string] {
services $action $id
}
def "main sv" [action?: string, id?: string] { main services $action $id }
# ── Init ──────────────────────────────────────────────────────────────────────
def "main init" [] {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let cfg_path = $"($root)/.ontoref/config.ncl"
let init_entry = if ($cfg_path | path exists) {
let cfg = (daemon-export-safe $cfg_path)
if $cfg != null {
$cfg | get actor_init? | default [] | where { |e| $e.actor == $actor } | first | default null
} else { null }
} else { null }
if $init_entry != null and ($init_entry.auto_run? | default false) and ($init_entry.mode? | default "" | is-not-empty) {
print $" init: running '($init_entry.mode)' for actor ($actor)"
run-mode ($init_entry.mode)
} else {
print $" Actor: ($actor) — no init mode configured"
print $" Run 'onref help mode' to see available modes"
}
}

View File

@ -0,0 +1,51 @@
# Validation contracts for reflection modes.
# These are applied AFTER the schema contract — they enforce semantic invariants
# that Nickel's structural typing cannot express alone.
# Pattern mirrors jpl_ontology/adrs/constraints.ncl: separate file, pure contracts.
let _non_empty_steps = std.contract.custom (
fun label =>
fun value =>
if std.array.length value.steps == 0 then
'Error {
message = "Mode '%{value.id}': steps must not be empty — a mode with no steps is passive documentation, not an executable contract"
}
else
'Ok value
) in
let _valid_trigger = std.contract.custom (
fun label =>
fun value =>
if std.string.length value.trigger == 0 then
'Error {
message = "Mode '%{value.id}': trigger must not be empty — it identifies how this mode is invoked"
}
else
'Ok value
) in
# Ensures every step that declares a cmd actually has a meaningful command (not whitespace-only).
let _non_empty_cmds = std.contract.custom (
fun label =>
fun value =>
let bad = value.steps
|> std.array.filter (fun s =>
std.record.has_field "cmd" s
&& std.string.length (std.string.trim s.cmd) == 0
)
|> std.array.map (fun s => s.id)
in
if std.array.length bad > 0 then
'Error {
message = "Mode '%{value.id}': steps with empty cmd: %{std.string.join ", " bad}"
}
else
'Ok value
) in
{
NonEmptySteps = _non_empty_steps,
ValidTrigger = _valid_trigger,
NonEmptyCmds = _non_empty_cmds,
}

26
reflection/defaults.ncl Normal file
View File

@ -0,0 +1,26 @@
let s = import "schema.ncl" in
let c = import "constraints.ncl" in
# Factory functions for reflection types.
# Pattern mirrors jpl_ontology/adrs/defaults.ncl: import schema + constraints, expose factories.
{
# make_mode ActionContract data → validates structural contract + semantic constraints.
# The Mode contract enforces DAG correctness (uniqueness + referential integrity).
# Cycle detection runs separately in ontoref-reflection::dag::validate.
make_mode = fun ActionContract => fun data =>
let result | (s.Mode ActionContract) = data in
let result | c.NonEmptySteps = result in
let result | c.ValidTrigger = result in
result,
# make_step ActionContract data → validates a single step against the schema.
make_step = fun ActionContract => fun data =>
data | (s.ActionStep ActionContract),
# Re-export raw types for consumers that apply contracts manually.
Mode = s.Mode,
ActionStep = s.ActionStep,
Dependency = s.Dependency,
OnError = s.OnError,
}

View File

@ -0,0 +1,89 @@
# Form: Onboard an existing project into the ontoref protocol
# Drives reflection/modes/adopt_ontoref.ncl with concrete parameter values.
#
# Usage:
# nickel export reflection/forms/adopt_ontoref.ncl
# # Fill in values, then run the generated adopt_ontoref.nu script.
#
# Agent query:
# nickel export reflection/forms/adopt_ontoref.ncl \
# | get elements \
# | where type != "section_header" \
# | where type != "section" \
# | select name prompt required default
{
name = "Adopt Ontoref",
description = "Onboard an existing project into the ontoref protocol. Adds .ontoref/, .ontology/ stubs, and scripts/ontoref wrapper without overwriting existing files.",
elements = [
# ── Identity ─────────────────────────────────────────────────────────────
{ type = "section_header", name = "identity_header",
title = "Project Identity", border_top = true, border_bottom = true },
{ type = "text", name = "project_name",
prompt = "Project name (identifier)",
required = true,
placeholder = "my-service",
help = "Lowercase, kebab-case. Used in .ontology/ node IDs and NATS subject prefix." },
{ type = "text", name = "project_dir",
prompt = "Absolute path to the existing project",
required = true,
placeholder = "/Users/Akasha/Development/my-service",
help = "The directory must exist. Ontoref only adds files — never overwrites." },
# ── Ontoref location ─────────────────────────────────────────────────────
{ type = "section_header", name = "ontoref_header",
title = "Ontoref Location", border_top = true, border_bottom = true },
{ type = "text", name = "ontoref_dir",
prompt = "Absolute path to ontoref checkout",
required = true,
default = "/Users/Akasha/Development/ontoref",
help = "Used to source templates (ontology stubs, config template, scripts wrapper)." },
# ── What to install ──────────────────────────────────────────────────────
{ type = "section_header", name = "install_header",
title = "What to Install", border_top = true, border_bottom = true },
{ type = "section", name = "install_note",
content = "All steps are additive and idempotent. Existing files are never overwritten." },
{ type = "confirm", name = "install_config",
prompt = "Create .ontoref/config.ncl (per-project ontoref config)?",
default = true,
help = "Templated from ontoref/templates/ontoref-config.ncl. Controls log, mode_run rules, NATS events." },
{ type = "confirm", name = "install_ontology_stubs",
prompt = "Create .ontology/ stubs (core.ncl, state.ncl, gate.ncl)?",
default = true,
help = "Stub files are minimal — project owner fills in project-specific content. Skipped if files exist." },
{ type = "confirm", name = "install_scripts_wrapper",
prompt = "Install scripts/ontoref thin wrapper?",
default = true,
help = "Thin bash wrapper that sets ONTOREF_ROOT and ONTOREF_PROJECT_ROOT, then delegates to ontoref entry point." },
# ── Validation ───────────────────────────────────────────────────────────
{ type = "section_header", name = "validation_header",
title = "Validation", border_top = true, border_bottom = true },
{ type = "confirm", name = "validate_after",
prompt = "Run nickel export on .ontology/ files after installation?",
default = true,
help = "Verifies the stub files parse correctly with nickel." },
# ── Review ───────────────────────────────────────────────────────────────
{ type = "section_header", name = "review_header",
title = "Review", border_top = true },
{ type = "section", name = "review_note",
content = "The generated script will:\n 1. mkdir -p .ontoref/logs .ontoref/locks (idempotent)\n 2. Copy .ontoref/config.ncl from template (if not present)\n 3. Copy .ontology/{core,state,gate}.ncl stubs (if not present)\n 4. Install scripts/ontoref wrapper (if not present)\n 5. Run nickel export on .ontology/ files to validate\n\nFiles already present are NOT overwritten." },
{ type = "confirm", name = "ready_to_generate",
prompt = "Generate the adopt script?",
default = true },
],
}

View File

@ -0,0 +1,92 @@
{
name = "Backlog Item",
description = "Add a new item to reflection/backlog.ncl. Captures intent, priority, and graduation path.",
display_mode = "complete",
elements = [
{ type = "section_header", name = "identity_header",
title = "Item", border_top = true, border_bottom = true },
{ type = "text", name = "title",
prompt = "Title",
required = true,
placeholder = "Migrate examples/ to reflection/forms/",
help = "Imperative present tense. What needs to happen.",
nickel_path = ["title"] },
{ type = "select", name = "kind",
prompt = "Kind",
default = "Todo",
options = [
{ value = "Todo", label = "Todo — concrete task with clear done state" },
{ value = "Wish", label = "Wish — desirable but not yet decided" },
{ value = "Idea", label = "Idea — worth exploring, not committed" },
{ value = "Bug", label = "Bug — known defect to fix" },
{ value = "Debt", label = "Debt — technical debt to address" },
],
nickel_path = ["kind"] },
{ type = "select", name = "priority",
prompt = "Priority",
default = "Medium",
options = [
{ value = "Critical", label = "Critical — blocks other work" },
{ value = "High", label = "High — important, do soon" },
{ value = "Medium", label = "Medium — normal queue" },
{ value = "Low", label = "Low — nice to have" },
],
nickel_path = ["priority"] },
{ type = "editor", name = "detail",
prompt = "Detail",
required = false,
file_extension = "md",
prefix_text = "# What needs to happen and why?\n# How will done be verified?\n\n",
nickel_path = ["detail"] },
{ type = "section_header", name = "links_header",
title = "Links to existing artifacts", border_top = true, border_bottom = true },
{ type = "text", name = "related_adrs",
prompt = "Related ADRs (comma-separated)",
required = false,
placeholder = "adr-001,adr-005",
nickel_path = ["related_adrs"] },
{ type = "text", name = "related_dim",
prompt = "Related ontology dimension ID",
required = false,
placeholder = "reflection-modes-coverage",
help = "Query: nickel export .ontology/state.ncl | jq '.dimensions[].id'",
nickel_path = ["related_dim"] },
{ type = "text", name = "related_config",
prompt = "Related config seal ID (if this item tracks a config state)",
required = false,
placeholder = "cfg-001",
help = "The cfg-NNN seal this item is linked to. Query: ontoref config history <profile>",
nickel_path = ["related_config"] },
{ type = "section_header", name = "graduation_header",
title = "Graduation path", border_top = true, border_bottom = true },
{ type = "section", name = "graduation_note",
content = "When this item is ready, what does it become? This determines what 'ontoref backlog promote' will do." },
{ type = "select", name = "graduates_to",
prompt = "Graduates to",
options = [
{ value = "Adr", label = "ADR — becomes an architectural decision record" },
{ value = "Mode", label = "Mode — becomes a reflection mode with verifiable steps" },
{ value = "StateTransition", label = "StateTransition — moves an ontology dimension forward" },
{ value = "PrItem", label = "PrItem — becomes a PR checklist entry" },
],
nickel_path = ["graduates_to"] },
{ type = "confirm", name = "ready",
prompt = "Add this item to backlog.ncl?",
default = true,
nickel_path = ["ready"] },
],
}

View File

@ -0,0 +1,198 @@
# Form: Create or edit an ADR
# Backend: CLI / TUI / Web / Agent
#
# Roundtrip:
# typedialog nickel-roundtrip \
# --input adrs/_template.ncl \
# --form reflection/forms/new_adr.ncl \
# --output adrs/adr-NNN-title.ncl \
# --ncl-template reflection/templates/adr.ncl.j2
#
# Agent query (get field structure without running a form):
# nickel export reflection/forms/new_adr.ncl \
# | get elements \
# | where type != "section_header" \
# | where type != "section" \
# | select name prompt nickel_path required default
{
name = "New ADR",
description = "Author or edit an Architectural Decision Record. Produces a validated Nickel file at adrs/adr-NNN-title.ncl.",
display_mode = "complete",
elements = [
# ── Identity ─────────────────────────────────────────────────────────────
{ type = "section_header", name = "identity_header",
title = "ADR Identity", border_top = true, border_bottom = true },
{ type = "text", name = "id",
prompt = "ADR identifier",
required = true,
help = "Format: adr-NNN. Run `ls adrs/adr-*.ncl | sort` to find the next number.",
nickel_path = ["id"] },
{ type = "text", name = "title",
prompt = "Title",
required = true,
placeholder = "Declarative statement of the decision (not a question)",
help = "Should complete the sentence: 'We decided that...'",
nickel_path = ["title"] },
{ type = "select", name = "status",
prompt = "Status",
default = "Proposed",
options = [
{ value = "Proposed", label = "Proposed — draft, not yet authoritative" },
{ value = "Accepted", label = "Accepted — active, constraints are live" },
{ value = "Superseded", label = "Superseded — replaced by another ADR (keep file)" },
{ value = "Deprecated", label = "Deprecated — abandoned without replacement" },
],
nickel_path = ["status"] },
{ type = "text", name = "date",
prompt = "Date (YYYY-MM-DD)",
default = "2026-03-09",
placeholder = "2026-03-09",
nickel_path = ["date"] },
# ── Context & Decision ───────────────────────────────────────────────────
{ type = "section_header", name = "context_header",
title = "Context & Decision", border_top = true, border_bottom = true },
{ type = "editor", name = "context",
prompt = "Context",
required = true,
file_extension = "md",
prefix_text = "# What situation led to this decision?\n# What forces are at play?\n\n",
help = "Describe the situation without yet mentioning the decision.",
nickel_path = ["context"] },
{ type = "editor", name = "decision",
prompt = "Decision",
required = true,
file_extension = "md",
prefix_text = "# State the decision in one or two sentences.\n# Be declarative, not prescriptive.\n\n",
nickel_path = ["decision"] },
# ── Rationale ────────────────────────────────────────────────────────────
{ type = "section_header", name = "rationale_header",
title = "Rationale", border_top = true, border_bottom = true },
{ type = "editor", name = "rationale",
prompt = "Rationale entries (Nickel array)",
required = true,
file_extension = "ncl",
prefix_text = "# Each entry: { claim = \"...\", detail = \"...\" }\n# claim: one-line assertion. detail: the supporting argument.\n\n",
help = "At least one entry. claim answers 'why'; detail explains the mechanism.",
nickel_path = ["rationale"] },
# ── Constraints ──────────────────────────────────────────────────────────
{ type = "section_header", name = "constraints_header",
title = "Constraints (active checks)", border_top = true, border_bottom = true },
{ type = "section", name = "constraints_note",
content = "Every ADR requires at least one Hard constraint. check_hint must be an executable command — not prose. The constraint is what makes the ADR machine-verifiable." },
{ type = "editor", name = "constraints",
prompt = "Constraints (Nickel array)",
required = true,
file_extension = "ncl",
prefix_text = "# Required fields per entry:\n# id = \"kebab-case-id\",\n# claim = \"What must be true\",\n# scope = \"Where this applies\",\n# severity = 'Hard, # Hard | Soft\n# check_hint = \"executable command that returns non-zero on violation\",\n# rationale = \"Why this constraint\",\n\n",
help = "Hard: non-negotiable, blocks a change. Soft: guideline, requires justification to bypass.",
nickel_path = ["constraints"] },
# ── Consequences ─────────────────────────────────────────────────────────
{ type = "section_header", name = "consequences_header",
title = "Consequences", border_top = true, border_bottom = true },
{ type = "editor", name = "consequences_positive",
prompt = "Positive consequences (Nickel array of strings)",
required = true,
file_extension = "ncl",
prefix_text = "# [ \"One benefit\", \"Another benefit\" ]\n\n",
nickel_path = ["consequences", "positive"] },
{ type = "editor", name = "consequences_negative",
prompt = "Negative consequences / trade-offs (Nickel array of strings)",
required = true,
file_extension = "ncl",
prefix_text = "# [ \"One trade-off\", \"Another trade-off\" ]\n\n",
nickel_path = ["consequences", "negative"] },
# ── Alternatives ─────────────────────────────────────────────────────────
{ type = "section_header", name = "alternatives_header",
title = "Alternatives Considered", border_top = true, border_bottom = true },
{ type = "editor", name = "alternatives_considered",
prompt = "Alternatives (Nickel array)",
required = true,
file_extension = "ncl",
prefix_text = "# Each entry: { option = \"Name\", why_rejected = \"Reason\" }\n\n",
nickel_path = ["alternatives_considered"] },
# ── Ontology check ───────────────────────────────────────────────────────
{ type = "section_header", name = "ontology_header",
title = "Ontology Check", border_top = true, border_bottom = true },
{ type = "section", name = "ontology_note",
content = "Run before filling: nickel export .ontology/core.ncl | get nodes | where invariant == true | get id" },
{ type = "text", name = "ontology_decision_string",
prompt = "Decision string (one line, imperative)",
required = true,
placeholder = "ontoref provides X — each project does Y independently",
help = "Concise restatement of the decision for ontology cross-reference.",
nickel_path = ["ontology_check", "decision_string"] },
{ type = "multiselect", name = "ontology_invariants_at_risk",
prompt = "Invariants at risk (select all that apply)",
help = "Which core invariants does this decision touch? Query: nickel export .ontology/core.ncl | get nodes | where invariant == true | get id",
options = [
{ value = "no-runtime-conditioning", label = "no-runtime-conditioning" },
{ value = "layer-autonomy", label = "layer-autonomy" },
{ value = "project-sovereignty", label = "project-sovereignty" },
{ value = "nats-surreal-only-external", label = "nats-surreal-only-external" },
{ value = "pattern-not-enforcer", label = "pattern-not-enforcer" },
],
nickel_path = ["ontology_check", "invariants_at_risk"] },
{ type = "select", name = "ontology_verdict",
prompt = "Verdict",
default = "Safe",
options = [
{ value = "Safe", label = "Safe — no invariant is at risk" },
{ value = "RequiresJustification", label = "RequiresJustification — invariant is touched, justification must follow" },
],
nickel_path = ["ontology_check", "verdict"] },
# ── Related ADRs ─────────────────────────────────────────────────────────
{ type = "section_header", name = "related_header",
title = "Related & Supersession", border_top = true, border_bottom = true },
{ type = "text", name = "related_adrs",
prompt = "Related ADRs (comma-separated, e.g. adr-002,adr-003)",
required = false,
placeholder = "adr-002,adr-003",
help = "ADRs that this decision builds on or responds to.",
nickel_path = ["related_adrs"] },
{ type = "text", name = "supersedes",
prompt = "Supersedes (adr-NNN, if this replaces another ADR)",
required = false,
placeholder = "adr-002",
help = "If set, also edit the old ADR: set status = Superseded and superseded_by = this id.",
nickel_path = ["supersedes"] },
# ── Review ───────────────────────────────────────────────────────────────
{ type = "section_header", name = "review_header",
title = "Review", border_top = true },
{ type = "section", name = "review_note",
content = "After saving: nickel typecheck adrs/adr-NNN-title.ncl\nOnly set status = Accepted after human review. Proposed ADRs are not authoritative." },
{ type = "confirm", name = "ready_to_save",
prompt = "All fields complete — generate the ADR file?",
default = true },
],
}

View File

@ -0,0 +1,139 @@
# Form: Initialize a new project in the ontoref ecosystem
# Drives reflection/modes/new_project.ncl with concrete parameter values.
#
# No roundtrip input (project doesn't exist yet).
# Usage:
# typedialog form reflection/forms/new_project.ncl \
# --ncl-template reflection/templates/create_project.nu.j2 \
# --output scripts/create-{project_name}.nu
#
# Agent query:
# nickel export reflection/forms/new_project.ncl \
# | get elements \
# | where type != "section_header" \
# | where type != "section" \
# | select name prompt required default
{
name = "New Project",
description = "Initialize a new project in the ecosystem. Produces a Nushell script ready to execute.",
elements = [
# ── Identity ─────────────────────────────────────────────────────────────
{ type = "section_header", name = "identity_header",
title = "Project Identity", border_top = true, border_bottom = true },
{ type = "text", name = "project_name",
prompt = "Project name (identifier)",
required = true,
placeholder = "my-service",
help = "Lowercase, kebab-case. Used as NATS subject prefix (ecosystem.{name}.*), kogral id, syntaxis project name." },
{ type = "text", name = "project_description",
prompt = "One-line description",
required = true,
placeholder = "Handles user authentication for the ecosystem" },
{ type = "select", name = "project_type",
prompt = "Project type",
required = true,
options = [
{ value = "rust-service", label = "Rust binary service (has NATS + SurrealDB)" },
{ value = "rust-library", label = "Rust library crate (compile-time dependency only)" },
{ value = "nu-scripts", label = "Nushell scripts (operational automation)" },
{ value = "mixed", label = "Mixed (Rust + Nushell + Nickel)" },
] },
# ── Paths ────────────────────────────────────────────────────────────────
{ type = "section_header", name = "paths_header",
title = "Paths", border_top = true, border_bottom = true },
{ type = "text", name = "project_dir",
prompt = "Absolute path for the new project",
required = true,
placeholder = "/Users/Akasha/Development/my-service",
help = "Parent directory must exist. The directory itself must not exist yet." },
{ type = "text", name = "ontoref_dir",
prompt = "Absolute path to ontoref checkout",
required = true,
default = "/Users/Akasha/Development/ontoref",
help = "Used to copy .ontology/ templates and reflection/schema.ncl." },
# ── Git ──────────────────────────────────────────────────────────────────
{ type = "section_header", name = "git_header",
title = "Git", border_top = true, border_bottom = true },
{ type = "text", name = "git_remote_url",
prompt = "Remote git URL (optional, added after init)",
required = false,
placeholder = "https://repo.jesusperez.pro/jesus/my-service",
help = "Leave blank to skip remote setup." },
{ type = "text", name = "default_branch",
prompt = "Default branch name",
default = "main",
required = true },
# ── Ecosystem tools ──────────────────────────────────────────────────────
{ type = "section_header", name = "tools_header",
title = "Ecosystem Tools", border_top = true, border_bottom = true },
{ type = "section", name = "tools_note",
content = "All tools are optional. The project functions without them.\nADR-003: only NATS and SurrealDB are shared external runtime dependencies." },
{ type = "confirm", name = "use_nats",
prompt = "Configure NATS stream (ecosystem.{project_name}.*)?",
default = true,
help = "Creates the NATS stream for the project's ecosystem subjects. Idempotent." },
{ type = "text", name = "nats_url",
prompt = "NATS server URL",
default = "nats://localhost:4222",
when = "use_nats == true" },
{ type = "confirm", name = "use_surrealdb",
prompt = "Note SurrealDB namespace for this project?",
default = false,
help = "Does not create the namespace — just records it in the generated script." },
{ type = "text", name = "surrealdb_namespace",
prompt = "SurrealDB namespace",
when = "use_surrealdb == true",
placeholder = "my-service" },
{ type = "confirm", name = "use_kogral",
prompt = "Initialize kogral graph (.kogral/)?",
default = false,
help = "kogral init is optional. Execution continues if kogral is not installed." },
{ type = "confirm", name = "use_syntaxis",
prompt = "Register in syntaxis?",
default = false,
help = "syntaxis project create is optional. Execution continues if syntaxis is not installed." },
# ── Ontology ─────────────────────────────────────────────────────────────
{ type = "section_header", name = "ontology_header",
title = "Project Ontology", border_top = true, border_bottom = true },
{ type = "section", name = "ontology_note",
content = "The .ontology/ templates are copied from ontoref. The project owner fills in:\n core.ncl — invariants, tensions, practices specific to this project\n state.ncl — current and desired states per dimension\n gate.ncl — active membranes protecting key conditions" },
{ type = "confirm", name = "copy_ontology_template",
prompt = "Copy .ontology/ templates from ontoref?",
default = true,
help = "Copies core.ncl, state.ncl, gate.ncl stubs. Project owner fills in project-specific content." },
# ── Review ───────────────────────────────────────────────────────────────
{ type = "section_header", name = "review_header",
title = "Review", border_top = true },
{ type = "section", name = "review_note",
content = "The generated script will:\n 1. git init {project_dir}\n 2. Copy .ontology/ templates (if selected)\n 3. Configure NATS stream (if selected)\n 4. Register in kogral / syntaxis (if selected, optional)\n 5. Publish ecosystem.project.created to NATS (best-effort)" },
{ type = "confirm", name = "ready_to_generate",
prompt = "Generate the create-project script?",
default = true },
],
}

View File

@ -0,0 +1,70 @@
# Form: Query active ADR constraints
# Drives the agent/developer through selecting what they need to validate.
# No roundtrip — output is a Nushell command to run.
#
# Agent query:
# nickel export reflection/forms/query_constraints.ncl \
# | get elements \
# | where type != "section_header" \
# | where type != "section" \
# | select name prompt options
{
name = "Query ADR Constraints",
description = "Select what you need and get the exact command to run. No ADR reading required.",
elements = [
{ type = "section_header", name = "intent_header",
title = "What do you need to validate?", border_top = true, border_bottom = true },
{ type = "select", name = "query_intent",
prompt = "Intent",
required = true,
options = [
{ value = "active_hard", label = "Active Hard constraints — what I cannot violate right now" },
{ value = "active_all", label = "Active constraint set — all Accepted ADRs" },
{ value = "point_in_time", label = "Historical — what constraints were active at a given date" },
{ value = "supersession", label = "Supersession chain — trace what replaced what" },
{ value = "invariants", label = "Core invariants — what is unconditionally protected" },
] },
{ type = "text", name = "point_in_time_date",
prompt = "Date for historical query (YYYY-MM)",
required = false,
default = "2026-01",
when = "query_intent == point_in_time",
help = "Returns the constraint set that was active on or before this date." },
{ type = "text", name = "adr_id_filter",
prompt = "Filter to specific ADR (e.g. adr-002, leave blank for all)",
required = false,
placeholder = "adr-002",
when = "query_intent == active_hard || query_intent == active_all",
help = "Leave blank to query across all Accepted ADRs." },
{ type = "section_header", name = "output_header",
title = "Command to run", border_top = true, border_bottom = true },
{ type = "section", name = "cmd_active_hard",
when = "query_intent == active_hard",
content = "Run:\n ls adrs/adr-*.ncl | each { |f| nickel export $f } | where status == 'Accepted | get constraints | flatten | where severity == 'Hard | select id claim check_hint" },
{ type = "section", name = "cmd_active_all",
when = "query_intent == active_all",
content = "Run:\n ls adrs/adr-*.ncl | each { |f| nickel export $f } | where status == 'Accepted | each { |a| { adr: $a.id, title: $a.title, constraints: $a.constraints } }" },
{ type = "section", name = "cmd_point_in_time",
when = "query_intent == point_in_time",
content = "Run (replace YYYY-MM with your target date):\n ls adrs/adr-*.ncl | each { |f| nickel export $f } | where (($it.status == 'Accepted or $it.status == 'Superseded) and $it.date <= \"YYYY-MM\") | where superseded_by? == null | get constraints | flatten" },
{ type = "section", name = "cmd_supersession",
when = "query_intent == supersession",
content = "Run:\n ls adrs/adr-*.ncl | each { |f| nickel export $f } | select id status supersedes superseded_by date | sort-by date" },
{ type = "section", name = "cmd_invariants",
when = "query_intent == invariants",
content = "Run:\n nickel export .ontology/core.ncl | get nodes | where invariant == true | select id name description" },
],
}

View File

@ -0,0 +1,145 @@
{
name = "Register Change",
description = "Record a meaningful change and synchronize reflection, ontology, and changelog. Run after writing code, before git commit.",
display_mode = "complete",
elements = [
# ── Summary ──────────────────────────────────────────────────────────────
{ type = "section_header", name = "summary_header",
title = "Change Summary", border_top = true, border_bottom = true },
{ type = "text", name = "summary",
prompt = "One-line summary of the change",
required = true,
placeholder = "Add verifiable flag to mode step schema",
help = "Imperative present tense. This becomes the CHANGELOG entry title.",
nickel_path = ["summary"] },
{ type = "select", name = "change_type",
prompt = "Change type",
required = true,
options = [
{ value = "feature", label = "Feature — new capability or behavior" },
{ value = "fix", label = "Fix — bug correction" },
{ value = "architectural", label = "Architectural — decision that touches invariants or shapes future choices" },
{ value = "refactor", label = "Refactor — internal restructure, no behavior change" },
{ value = "tooling", label = "Tooling — scripts, CI, reflection infrastructure" },
{ value = "docs", label = "Docs — documentation only (no code change)" },
],
nickel_path = ["change_type"] },
{ type = "editor", name = "detail",
prompt = "Detail (optional — expands the CHANGELOG entry)",
required = false,
file_extension = "md",
prefix_text = "# What changed, why, and any caveats.\n\n",
nickel_path = ["detail"] },
# ── Architectural decision ────────────────────────────────────────────────
{ type = "section_header", name = "adr_header",
title = "Architectural Decision (if applicable)", border_top = true, border_bottom = true },
{ type = "section", name = "adr_note",
content = "Fill only if change_type = architectural OR if this change formalizes a decision already embedded in the code." },
{ type = "confirm", name = "needs_adr",
prompt = "Does this change require a new ADR?",
default = false,
nickel_path = ["needs_adr"] },
{ type = "text", name = "adr_title",
prompt = "ADR title (if needs_adr)",
required = false,
placeholder = "Reflection modes replace examples/ as verifiable knowledge source",
help = "Will be passed to /create-adr. Leave empty if needs_adr = false.",
nickel_path = ["adr_title"] },
{ type = "confirm", name = "adr_accept_immediately",
prompt = "Accept ADR immediately? (-a flag)",
default = false,
help = "Use when the decision is already implemented and there is no ambiguity.",
nickel_path = ["adr_accept_immediately"] },
# ── Ontology state ───────────────────────────────────────────────────────
{ type = "section_header", name = "ontology_header",
title = "Ontology State (if applicable)", border_top = true, border_bottom = true },
{ type = "section", name = "ontology_note",
content = "Fill if this change moves a dimension forward. Query current state: nickel export .ontology/state.ncl | jq '.dimensions[] | .id + \": \" + .current_state'" },
{ type = "confirm", name = "changes_ontology_state",
prompt = "Does this change move an ontology dimension to a new state?",
default = false,
nickel_path = ["changes_ontology_state"] },
{ type = "text", name = "ontology_dimension_id",
prompt = "Dimension ID (if changes_ontology_state)",
required = false,
placeholder = "reflection-modes-coverage",
nickel_path = ["ontology_dimension_id"] },
{ type = "text", name = "ontology_new_state",
prompt = "New current_state value",
required = false,
placeholder = "active",
nickel_path = ["ontology_new_state"] },
# ── Capability / Mode ────────────────────────────────────────────────────
{ type = "section_header", name = "capability_header",
title = "Capability / Mode (if applicable)", border_top = true, border_bottom = true },
{ type = "confirm", name = "affects_capability",
prompt = "Does this change add, modify, or remove a user-visible capability?",
default = false,
nickel_path = ["affects_capability"] },
{ type = "select", name = "capability_action",
prompt = "Capability action",
default = "add",
options = [
{ value = "add", label = "Add — new capability" },
{ value = "modify", label = "Modify — existing capability changed" },
{ value = "remove", label = "Remove — capability no longer exists" },
],
nickel_path = ["capability_action"] },
{ type = "text", name = "capability_mode_id",
prompt = "Mode ID to create or update (if affects_capability)",
required = false,
placeholder = "ontoref-register",
help = "Matches filename in reflection/modes/<id>.ncl",
nickel_path = ["capability_mode_id"] },
# ── Config Profile ───────────────────────────────────────────────────────
{ type = "section_header", name = "config_header",
title = "Config Profile (if applicable)", border_top = true, border_bottom = true },
{ type = "section", name = "config_note",
content = "Fill if this change modified a sealed config profile (development, ci, staging, production).\nontoref config apply will seal the profile and write a history entry linked to this change." },
{ type = "confirm", name = "seals_config_profile",
prompt = "Does this change modify a config profile that should be resealed?",
default = false,
nickel_path = ["seals_config_profile"] },
{ type = "text", name = "config_profile",
prompt = "Profile name (if seals_config_profile)",
required = false,
placeholder = "development",
help = "Lowercase profile name: development | ci | staging | production",
nickel_path = ["config_profile"] },
# ── Confirm ──────────────────────────────────────────────────────────────
{ type = "section_header", name = "confirm_header",
title = "Confirm", border_top = true },
{ type = "section", name = "confirm_note",
content = "register.nu will write:\n - CHANGELOG entry (always)\n - ADR stub if needs_adr = true\n - Patch .ontology/state.ncl if changes_ontology_state = true\n - Mode stub if affects_capability = true and capability_action = add\nAll files are typechecked before write." },
{ type = "confirm", name = "ready",
prompt = "Write all artifacts?",
default = true,
nickel_path = ["ready"] },
],
}

View File

@ -0,0 +1,121 @@
# Form: Supersede an existing ADR
# Manages the ADR lifecycle: Accepted → Superseded, with bidirectional linking.
#
# This form drives two file edits:
# 1. Old ADR: status = Superseded, superseded_by = new_adr_id
# 2. New ADR: must have supersedes = old_adr_id (verified post-form)
#
# Usage:
# typedialog form reflection/forms/supersede_adr.ncl \
# --ncl-template reflection/templates/supersede_adr.nu.j2 \
# --output scripts/supersede-{old_adr_id}.nu
#
# Agent query:
# nickel export reflection/forms/supersede_adr.ncl \
# | get elements \
# | where type != "section_header" \
# | where type != "section" \
# | select name prompt required
{
name = "Supersede ADR",
description = "Mark an existing Accepted ADR as superseded and link it to the replacing ADR. Preserves the historical record — files are never deleted.",
elements = [
{ type = "section_header", name = "context_header",
title = "Current ADR landscape", border_top = true, border_bottom = true },
{ type = "section", name = "list_cmd",
content = "Run first to see the active ADR set:\n ls adrs/adr-*.ncl | each { |f| nickel export $f } | select id title status date | sort-by id" },
# ── Old ADR (being superseded) ────────────────────────────────────────────
{ type = "section_header", name = "old_header",
title = "ADR being superseded", border_top = true, border_bottom = true },
{ type = "text", name = "old_adr_id",
prompt = "ADR to supersede (e.g. adr-002)",
required = true,
placeholder = "adr-002",
help = "Must be in Accepted status. Run the command above to verify." },
{ type = "text", name = "old_adr_file",
prompt = "Path to that ADR file",
required = true,
placeholder = "adrs/adr-002-library-not-runtime.ncl",
help = "The file that will have status changed to Superseded." },
# ── New ADR (the replacement) ─────────────────────────────────────────────
{ type = "section_header", name = "new_header",
title = "Replacing ADR", border_top = true, border_bottom = true },
{ type = "text", name = "new_adr_id",
prompt = "New ADR identifier (e.g. adr-007)",
required = true,
placeholder = "adr-007",
help = "The ADR that supersedes the old one. Must already exist or be created before running this form." },
{ type = "text", name = "new_adr_file",
prompt = "Path to the new ADR file",
required = true,
placeholder = "adrs/adr-007-updated-library-model.ncl",
help = "Must contain supersedes = \"old_adr_id\" and export cleanly." },
{ type = "confirm", name = "new_adr_exports",
prompt = "Confirmed: new ADR exports cleanly with `nickel export {new_adr_file}`",
required = true,
help = "Do not proceed until the new ADR typechecks. Status will not be set to Accepted here — do that manually after review." },
{ type = "confirm", name = "new_has_supersedes_field",
prompt = "Confirmed: new ADR has `supersedes = \"{old_adr_id}\"` in its body",
required = true,
help = "Required for bidirectional chain integrity. Without this, point-in-time queries break." },
# ── Reason ───────────────────────────────────────────────────────────────
{ type = "section_header", name = "reason_header",
title = "Supersession reason", border_top = true, border_bottom = true },
{ type = "select", name = "supersession_reason",
prompt = "Why is this ADR being superseded?",
required = true,
options = [
{ value = "architectural_evolution", label = "Architectural evolution — the decision changed" },
{ value = "constraint_update", label = "Constraint update — same decision, different constraints" },
{ value = "scope_change", label = "Scope change — decision now covers more/less" },
{ value = "correction", label = "Correction — previous ADR had an error" },
] },
{ type = "text", name = "supersession_note",
prompt = "One-line note for the git commit message",
required = true,
placeholder = "adr-002 superseded by adr-007: library model extended to cover schema exports" },
# ── Impact ───────────────────────────────────────────────────────────────
{ type = "section_header", name = "impact_header",
title = "Constraint impact", border_top = true, border_bottom = true },
{ type = "section", name = "impact_cmd",
content = "Check what the old ADR's Hard constraints covered:\n nickel export {old_adr_file} | get constraints | where severity == 'Hard | select id claim check_hint" },
{ type = "confirm", name = "constraints_reviewed",
prompt = "Confirmed: old ADR's Hard constraints have been reviewed and are covered by the new ADR",
required = true,
help = "The new ADR must re-state or explicitly drop any Hard constraint from the old ADR. No silent constraint removal." },
{ type = "confirm", name = "references_updated",
prompt = "Confirmed: no other file references the old ADR as if it were still active",
required = false,
help = "Run: grep -r \"{old_adr_id}\" adrs/ reflection/ .ontology/ .claude/ — review each match." },
# ── Execute ──────────────────────────────────────────────────────────────
{ type = "section_header", name = "execute_header",
title = "Generate patch script", border_top = true },
{ type = "section", name = "execute_note",
content = "The generated script will:\n 1. Edit {old_adr_file}: set status = Superseded, add superseded_by = \"{new_adr_id}\"\n 2. Verify both ADRs export cleanly\n 3. Print the git commit command to run" },
{ type = "confirm", name = "ready_to_generate",
prompt = "Generate the supersession script?",
default = true },
],
}

View File

@ -0,0 +1,38 @@
#!/usr/bin/env nu
# ontology-changed.nu - Handle ecosystem.ontology.changed events
# Triggers ontology-code synchronization
export def "handle" [payload: record]: nothing -> nothing {
let project = ($payload.project? | default ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT))
let decision = ($payload.decision? | default "auto")
let cyan = (ansi cyan)
let reset = (ansi reset)
print $" $cyan[ontology-changed]$reset Running synchronization for: ($project)"
# Execute sync with appropriate flags
let exec_result = (do {
# Switch to project directory
if ($project | path exists) and ($project | is-dir) {
cd $project
}
# Import and execute sync module
use ../modules/sync.nu *
# Run scan with appropriate flags based on decision
match $decision {
"strict" => { scan --strict },
"quick" => { scan --quick },
_ => { scan }
}
} | complete)
if $exec_result.exit_code == 0 {
let green = (ansi green)
print $" $green✓$reset Synchronization completed"
} else {
let err = ($exec_result.stderr? | default "unknown error")
error make { msg: $"Synchronization failed: $err" }
}
}

View File

@ -0,0 +1,36 @@
#!/usr/bin/env nu
# reflection-request.nu - Handle ecosystem.reflection.request events
# Triggers mode execution based on incoming event payload
export def "handle" [payload: record]: nothing -> nothing {
let mode_id = ($payload.mode_id? | default "")
let project = ($payload.project? | default ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT))
if ($mode_id | is-empty) {
error make { msg: "reflection-request: missing mode_id in payload" }
}
let cyan = (ansi cyan)
let reset = (ansi reset)
print $" $cyan[reflection-request]$reset Executing mode: ($mode_id)"
# Set project context for the mode execution
let exec_result = (do {
# Switch to project directory if specified
if ($project | path exists) and ($project | is-dir) {
cd $project
}
# Import and execute modes module - run-mode executes a mode by id
use ../nulib/modes.nu *
run-mode $mode_id
} | complete)
if $exec_result.exit_code == 0 {
let green = (ansi green)
print $" $green✓$reset Mode completed: ($mode_id)"
} else {
let err = ($exec_result.stderr? | default "unknown error")
error make { msg: $"Mode execution failed for ($mode_id): $err" }
}
}

View File

@ -0,0 +1,41 @@
#!/usr/bin/env nu
# reload.nu - Handle ecosystem.reflection.nushell.reload events
# Reloads configuration and clears caches
export def "handle" [payload: record]: nothing -> nothing {
let reason = ($payload.reason? | default "explicit request")
let project = ($payload.project? | default ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT))
let cyan = (ansi cyan)
let reset = (ansi reset)
print $" $cyan[reload]$reset Reloading configuration (reason: $reason)"
# Clear all environment caches
$env.NATS_AVAILABLE = ""
# Validate config syntax
if ($project | path exists) and ($project | is-dir) {
let config_path = $project + "/.ontoref/config.ncl"
if ($config_path | path exists) {
let gray = (ansi dark_gray)
print $" $grayConfig path: ($config_path)$reset"
let validate_result = (do {
env NICKEL_IMPORT_PATH=$project
^nickel typecheck $config_path
} | complete)
if $validate_result.exit_code == 0 {
let green = (ansi green)
print $" $green✓$reset Configuration is valid"
} else {
let err = ($validate_result.stderr? | default "syntax error")
error make { msg: $"Configuration validation failed: $err" }
}
}
}
let green = (ansi green)
print $" $green✓$reset Configuration cache cleared"
print $" $grayNote: Env changes apply to current shell only. Next command will reload.$reset"
}

View File

@ -0,0 +1,119 @@
let s = import "../schema.ncl" in
# Mode: adopt_ontoref
# Onboards an EXISTING project into the ontoref protocol.
# Creates missing pieces without overwriting existing files.
#
# Required params (substituted in cmd via {param}):
# {project_name} — identifier for this project (kebab-case)
# {project_dir} — absolute path to the existing project root
# {ontoref_dir} — absolute path to the ontoref checkout
{
id = "adopt_ontoref",
trigger = "Onboard an existing project into the ontoref protocol",
preconditions = [
"{project_dir} exists and is a directory",
"nickel is available in PATH",
"nu is available in PATH (>= 0.110.0)",
"{ontoref_dir}/templates/ontology/ exists (contains core.ncl, state.ncl, gate.ncl stubs)",
"{ontoref_dir}/templates/ontoref-config.ncl exists",
"{ontoref_dir}/templates/scripts-ontoref exists",
],
steps = [
{
id = "create_ontoref_dir",
action = "create_config_directory",
actor = 'Agent,
cmd = "mkdir -p {project_dir}/.ontoref/logs {project_dir}/.ontoref/locks",
depends_on = [],
on_error = { strategy = 'Stop },
note = "Creates .ontoref/ directory structure. mkdir -p is idempotent.",
},
{
id = "copy_ontoref_config",
action = "copy_config_template",
actor = 'Agent,
cmd = "test -f {project_dir}/.ontoref/config.ncl || sed 's/{{ project_name }}/{project_name}/g' {ontoref_dir}/templates/ontoref-config.ncl > {project_dir}/.ontoref/config.ncl",
depends_on = [{ step = "create_ontoref_dir", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Copies config template only if .ontoref/config.ncl does not already exist.",
},
{
id = "create_ontology_dir",
action = "create_ontology_directory",
actor = 'Agent,
cmd = "mkdir -p {project_dir}/.ontology",
depends_on = [{ step = "create_ontoref_dir", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Creates .ontology/ directory. Idempotent.",
},
{
id = "copy_ontology_core",
action = "copy_ontology_core_stub",
actor = 'Agent,
cmd = "test -f {project_dir}/.ontology/core.ncl || sed 's/{{ project_name }}/{project_name}/g' {ontoref_dir}/templates/ontology/core.ncl > {project_dir}/.ontology/core.ncl",
depends_on = [{ step = "create_ontology_dir", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Copies core.ncl stub. Skipped if file already exists.",
},
{
id = "copy_ontology_state",
action = "copy_ontology_state_stub",
actor = 'Agent,
cmd = "test -f {project_dir}/.ontology/state.ncl || sed 's/{{ project_name }}/{project_name}/g' {ontoref_dir}/templates/ontology/state.ncl > {project_dir}/.ontology/state.ncl",
depends_on = [{ step = "create_ontology_dir", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Copies state.ncl stub. Skipped if file already exists.",
},
{
id = "copy_ontology_gate",
action = "copy_ontology_gate_stub",
actor = 'Agent,
cmd = "test -f {project_dir}/.ontology/gate.ncl || sed 's/{{ project_name }}/{project_name}/g' {ontoref_dir}/templates/ontology/gate.ncl > {project_dir}/.ontology/gate.ncl",
depends_on = [{ step = "create_ontology_dir", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Copies gate.ncl stub. Skipped if file already exists.",
},
{
id = "install_scripts_wrapper",
action = "install_consumer_entry_point",
actor = 'Agent,
cmd = "mkdir -p {project_dir}/scripts && test -f {project_dir}/scripts/ontoref || (sed 's|{{ ontoref_dir }}|{ontoref_dir}|g' {ontoref_dir}/templates/scripts-ontoref > {project_dir}/scripts/ontoref && chmod +x {project_dir}/scripts/ontoref)",
depends_on = [{ step = "create_ontoref_dir", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Installs scripts/ontoref thin wrapper. Skipped if already present.",
},
{
id = "validate_ontology",
action = "nickel_typecheck_ontology",
actor = 'Agent,
cmd = "cd {project_dir} && nickel export .ontology/core.ncl > /dev/null && nickel export .ontology/state.ncl > /dev/null && nickel export .ontology/gate.ncl > /dev/null",
depends_on = [
{ step = "copy_ontology_core", kind = 'OnSuccess },
{ step = "copy_ontology_state", kind = 'OnSuccess },
{ step = "copy_ontology_gate", kind = 'OnSuccess },
],
on_error = { strategy = 'Stop },
note = "Validates all three .ontology/ files parse without errors.",
},
],
postconditions = [
"{project_dir}/.ontoref/config.ncl exists and is valid Nickel",
"{project_dir}/.ontology/core.ncl, state.ncl, gate.ncl exist and parse",
"{project_dir}/scripts/ontoref exists and is executable",
"No existing files were overwritten",
],
} | (s.Mode String)

View File

@ -0,0 +1,82 @@
let d = import "../defaults.ncl" in
d.make_mode String {
id = "coder-workflow",
trigger = "Manage .coder/ process memory — initialize authors, record structured entries, triage markdown, publish and graduate knowledge",
preconditions = [
"ONTOREF_PROJECT_ROOT is set",
"reflection/modules/coder.nu is accessible via ONTOREF_ROOT",
"Nushell >= 0.110.0 is available",
],
steps = [
{
id = "init-author",
action = "Initialize an author workspace with inbox/ and author.ncl. Each human, agent, or CI actor gets their own workspace.",
cmd = "./ontoref coder init <author> --actor Human|AgentClaude|AgentCustom|CI",
actor = 'Both,
on_error = { strategy = 'Stop },
},
{
id = "record-json",
action = "Write a structured JSON entry to entries.jsonl. Use for insights, summaries, completed features, decisions. Entries are immediately queryable via coder log.",
cmd = "./ontoref coder record <author> '<content>' --title '<title>' --kind info --category insights --tags '[tag1,tag2]' --relates_to '[nodo-id]' --trigger '<why>' --domain Architecture",
actor = 'Both,
depends_on = [{ step = "init-author" }],
on_error = { strategy = 'Continue },
},
{
id = "dump-markdown",
action = "Copy or paste markdown files into .coder/<author>/inbox/. Zero ceremony — just dump. Files are named YYYY-MM-DD-description.{kind}.md where kind is done|plan|info|review|audit|commit.",
actor = 'Human,
depends_on = [{ step = "init-author" }],
on_error = { strategy = 'Continue },
},
{
id = "triage",
action = "Classify inbox/ files into categories (insights, features, bugfixes, investigations, decisions, reviews, resources). Generates companion NCL with validated metadata. Use --interactive for manual review.",
cmd = "./ontoref coder triage <author> --interactive",
actor = 'Both,
depends_on = [{ step = "dump-markdown" }],
on_error = { strategy = 'Continue },
},
{
id = "query",
action = "Query JSONL entries across authors and categories. Filter by tag, kind, domain, or author. Export as JSON, JSONL, or CSV for external tools.",
cmd = "./ontoref coder log --author <author> --tag <tag> --domain <domain>",
actor = 'Both,
depends_on = [{ step = "record-json" }],
on_error = { strategy = 'Continue },
},
{
id = "publish",
action = "Promote entries from an author workspace to .coder/general/<category>/. Files are prefixed with author name for attribution.",
cmd = "./ontoref coder publish <author> <category>",
actor = 'Human,
depends_on = [{ step = "triage" }],
on_error = { strategy = 'Continue },
},
{
id = "graduate",
action = "Copy graduated entries from .coder/ to a committed path (reflection/knowledge/). Only categories marked graduable=true in context.ncl are eligible.",
cmd = "./ontoref coder graduate general/insights --target reflection/knowledge",
actor = 'Human,
depends_on = [{ step = "publish" }],
on_error = { strategy = 'Stop },
},
{
id = "register-ontology",
action = "After creating new systems or schemas, register them in .ontology/core.ncl with artifact_paths. Update state.ncl if maturity changed. Validate with nickel export.",
cmd = "nickel export .ontology/core.ncl && nickel export .ontology/state.ncl",
actor = 'Both,
on_error = { strategy = 'Stop },
},
],
postconditions = [
"coder authors lists all active author workspaces",
"coder log returns queryable structured entries",
"New systems have nodes in .ontology/core.ncl with artifact_paths",
],
}

View File

@ -0,0 +1,99 @@
let s = import "../schema.ncl" in
# Mode: create-pr
# Creates a GitHub/Gitea PR from the current branch.
# Body is generated from CHANGELOG.md [Unreleased] section — ontoref register
# must have been run before this mode to populate that section.
#
# Required params:
# {title} — PR title (defaults to branch name if not provided)
# {base} — base branch (default: main)
{
id = "create-pr",
trigger = "Open a pull request from current branch using CHANGELOG [Unreleased] as body",
preconditions = [
"gh is available in PATH and authenticated (gh auth status)",
"git working tree is clean (all changes committed)",
"CHANGELOG.md exists with a populated [Unreleased] section",
"ontoref register was run for all changes in this branch",
"Current branch is not main/master",
],
steps = [
{
id = "check-auth",
action = "Verify gh CLI is authenticated",
actor = 'Both,
cmd = "gh auth status",
verify = "gh auth status 2>&1 | grep -q 'Logged in'",
depends_on = [],
on_error = { strategy = 'Stop },
},
{
id = "check-unreleased",
action = "Verify CHANGELOG has content in [Unreleased] section",
actor = 'Agent,
cmd = "awk '/## \\[Unreleased\\]/,/## \\[/' CHANGELOG.md | grep -E '^- |^### '",
verify = "awk '/## \\[Unreleased\\]/,/## \\[/' CHANGELOG.md | grep -qE '^- |^### '",
depends_on = [],
on_error = { strategy = 'Stop },
},
{
id = "check-adr-validate",
action = "Run ADR constraint validation before PR",
actor = 'Both,
cmd = "source scripts/reflection.sh && nu \"${DISPATCHER}\" adr validate",
verify = "source scripts/reflection.sh && nu \"${DISPATCHER}\" adr validate 2>&1 | grep -qv 'FAIL'",
depends_on = [],
on_error = { strategy = 'Continue },
note = "Constraint failures are logged but do not block PR creation",
},
{
id = "extract-branch",
action = "Get current branch name for PR title fallback",
actor = 'Agent,
cmd = "git rev-parse --abbrev-ref HEAD",
verify = "git rev-parse --abbrev-ref HEAD | grep -qv 'main\\|master'",
depends_on = [{ step = "check-auth", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
},
{
id = "build-body",
action = "Extract [Unreleased] section from CHANGELOG as PR body",
actor = 'Agent,
cmd = "awk '/## \\[Unreleased\\]/{found=1; next} found && /^## \\[/{exit} found{print}' CHANGELOG.md | sed '/^$/N;/^\\n$/d'",
verify = "awk '/## \\[Unreleased\\]/{found=1; next} found && /^## \\[/{exit} found{print}' CHANGELOG.md | grep -qE '\\S'",
depends_on = [{ step = "check-unreleased", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
},
{
id = "create-pr",
action = "Open pull request via gh CLI",
actor = 'Both,
cmd = "gh pr create --base {base} --title \"{title}\" --body \"$(awk '/## \\[Unreleased\\]/{found=1; next} found && /^## \\[/{exit} found{print}' CHANGELOG.md)\"",
verify = "gh pr view --json number 2>/dev/null | jq '.number > 0'",
depends_on = [
{ step = "build-body", kind = 'OnSuccess },
{ step = "extract-branch", kind = 'OnSuccess },
{ step = "check-adr-validate", kind = 'Always },
],
on_error = { strategy = 'Stop },
},
{
id = "show-pr",
action = "Display PR URL",
actor = 'Both,
cmd = "gh pr view --json url | jq -r '.url'",
verify = "gh pr view --json url 2>/dev/null | jq -r '.url' | grep -qE '^http'",
depends_on = [{ step = "create-pr", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
},
],
postconditions = [
"PR is open and visible at the URL shown",
"PR body matches CHANGELOG [Unreleased] content",
"ADR constraints were validated before submission",
],
} | s.Mode std.string.NonEmpty

View File

@ -0,0 +1,54 @@
let s = import "../schema.ncl" in
# Mode: generate-examples
# Executes all verifiable steps from reflection/modes/ in the target project.
# The modes are the examples — this mode runs them to prove they work.
# Used as the integration test suite replacing a static examples/ directory.
{
id = "generate-examples",
trigger = "Run all verifiable mode steps as integration checks — modes are the canonical examples",
preconditions = [
"nickel is available in PATH",
"jq is available in PATH",
"reflection/modes/*.ncl export without error",
"Project binary (typedialog, ontoref, etc.) is installed and in PATH",
"reflection/forms/ contains at least one .ncl file",
],
steps = [
{
id = "list-verifiable-steps",
action = "Extract all steps with a non-empty verify field from all modes",
actor = 'Agent,
cmd = "for f in reflection/modes/*.ncl; do NICKEL_IMPORT_PATH=\"$(pwd)\" nickel export $f 2>/dev/null | jq -r '.steps[] | select(.verify != null and .verify != \"\") | .verify'; done",
verify = "for f in reflection/modes/*.ncl; do NICKEL_IMPORT_PATH=\"$(pwd)\" nickel export $f 2>/dev/null && true; done",
depends_on = [],
on_error = { strategy = 'Stop },
},
{
id = "run-verifiable-steps",
action = "Execute each verify command; non-zero exit fails the check",
actor = 'Agent,
cmd = "for f in reflection/modes/*.ncl; do NICKEL_IMPORT_PATH=\"$(pwd)\" nickel export $f 2>/dev/null | jq -r '.steps[] | select(.verify != null and .verify != \"\") | .verify' | while read -r check; do echo \"CHECK: $check\"; eval \"$check\" || { echo \"FAILED: $check\"; exit 1; }; done; done",
verify = "echo 'all verifiable steps passed'",
depends_on = [{ step = "list-verifiable-steps", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
},
{
id = "run-form-typecheck",
action = "Typecheck all forms in reflection/forms/",
actor = 'Both,
cmd = "for f in reflection/forms/*.ncl; do NICKEL_IMPORT_PATH=\"$(pwd)\" nickel typecheck $f || exit 1; done",
verify = "for f in reflection/forms/*.ncl; do NICKEL_IMPORT_PATH=\"$(pwd)\" nickel typecheck $f; done",
depends_on = [],
on_error = { strategy = 'Stop },
},
],
postconditions = [
"All modes with verify commands pass",
"All reflection/forms/*.ncl typecheck",
"No regressions in verifiable behavior",
],
} | s.Mode std.string.NonEmpty

View File

@ -0,0 +1,30 @@
let s = import "../schema.ncl" in
{
id = "generate-mdbook",
trigger = "Generate navigable mdBook documentation from ontology, ADRs, modes, and crates",
preconditions = [
"nickel is available in PATH",
".ontology/core.ncl exists and exports without error",
],
steps = [
{
id = "generate-mdbook",
action = "Extract all project data and generate docs/src/ + SUMMARY.md + build mdBook",
actor = 'Both,
cmd = "nu -c \"use ${ONTOREF_ROOT}/reflection/modules/generator.nu *; docs generate --fmt mdbook\"",
verify = "test -d docs/src && test -f docs/src/SUMMARY.md",
depends_on = [],
on_error = { strategy = 'Stop },
},
],
postconditions = [
"docs/src/SUMMARY.md exists with chapter structure",
"docs/src/architecture/overview.md reflects current .ontology/core.ncl",
"docs/src/decisions/ contains one page per ADR",
"docs/book/ contains navigable HTML if mdbook is installed",
],
} | s.Mode std.string.NonEmpty

View File

@ -0,0 +1,90 @@
let s = import "../schema.ncl" in
# Mode: new_project
# Creates a new project in the ecosystem with ontology, kogral, syntaxis, and NATS wiring.
#
# Required params (substituted in cmd via {param}):
# {project_name} — identifier used for kogral init and syntaxis project create
# {project_dir} — absolute path where the repository is created
# {ontoref_dir} — absolute path to the ontoref checkout (for ontology template)
{
id = "new_project",
trigger = "Initialize a new project in the ontoref ecosystem",
preconditions = [
"git is available in PATH",
"nickel is available in PATH",
"{project_dir} parent directory exists and is writable",
"kogral CLI available (optional — init_kogral continues on failure)",
"syntaxis CLI available (optional — create_syntaxis_project continues on failure)",
"nats CLI available (optional — configure_nats and publish_ecosystem_event continue on failure)",
"{ontoref_dir}/.ontology/ or templates/ontology/ exists",
],
steps = [
{
id = "init_repo",
action = "initialize_git_repository",
actor = 'Both,
cmd = "git -C {project_dir} init && git -C {project_dir} commit --allow-empty -m 'chore: initial commit'",
depends_on = [],
on_error = { strategy = 'Stop },
},
{
id = "copy_ontology_template",
action = "scaffold_ontology_directory",
actor = 'Agent,
cmd = "cp -r {ontoref_dir}/templates/ontology {project_dir}/.ontology",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Copies core.ncl, state.ncl, gate.ncl stubs — project owner fills in axioms",
},
{
id = "init_kogral",
action = "initialize_kogral_graph",
actor = 'Agent,
cmd = "kogral init --name {project_name} --dir {project_dir}/.kogral",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "kogral is optional — ecosystem functions without it; execution continues on failure",
},
{
id = "create_syntaxis_project",
action = "register_project_in_syntaxis",
actor = 'Agent,
cmd = "syntaxis project create --name {project_name} --path {project_dir}",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "syntaxis is optional — execution continues on failure",
},
{
id = "configure_nats",
action = "create_nats_consumer_for_project",
actor = 'Agent,
cmd = "^nats stream add ECOSYSTEM --subjects 'ecosystem.{project_name}.>' --no-headers-only --defaults err> /dev/null",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "NATS stream creation is idempotent and optional",
},
{
id = "publish_ecosystem_event",
action = "announce_project_created",
actor = 'Agent,
cmd = "nats pub ecosystem.project.created '{\"project_name\": \"{project_name}\", \"project_dir\": \"{project_dir}\"}'",
depends_on = [
{ step = "copy_ontology_template", kind = 'OnSuccess },
{ step = "init_kogral", kind = 'Always },
{ step = "create_syntaxis_project", kind = 'Always },
{ step = "configure_nats", kind = 'Always },
],
on_error = { strategy = 'Continue },
note = "Final announcement — best-effort, does not block project creation",
},
],
postconditions = [
"{project_dir} is an initialized git repository",
"{project_dir}/.ontology/ contains core.ncl, state.ncl, gate.ncl from templates",
"ecosystem.project.created published to NATS (best-effort)",
],
} | (s.Mode String)

View File

@ -0,0 +1,110 @@
let s = import "../schema.ncl" in
# Mode: new_service
# Creates a new Rust microservice in the ecosystem: ontology, kogral, syntaxis,
# NATS subject definition, infra provisioning, and ecosystem registration.
#
# Required params:
# {project_name} — service identifier (used in NATS subjects, syntaxis, kogral)
# {project_dir} — absolute path for the new service repository
# {ontoref_dir} — absolute path to the ontoref checkout
# {stack} — technology stack identifier (e.g. "rust-axum", "rust-tonic")
{
id = "new_service",
trigger = "Create a new Rust microservice in the ontoref ecosystem",
preconditions = [
"git is available in PATH",
"nickel is available in PATH",
"{project_dir} parent directory exists and is writable",
"nats CLI available (define_nats_subjects continues on failure)",
"kogral CLI available (init_kogral continues on failure)",
"syntaxis CLI available (create_syntaxis_project continues on failure)",
"provisioning system reachable (apply_infra continues on failure)",
"{ontoref_dir}/templates/ontology/ exists",
],
steps = [
{
id = "init_repo",
action = "initialize_git_repository",
actor = 'Both,
cmd = "git -C {project_dir} init && git -C {project_dir} commit --allow-empty -m 'chore: initial commit'",
depends_on = [],
on_error = { strategy = 'Stop },
},
{
id = "copy_ontology_template",
action = "scaffold_ontology_directory",
actor = 'Agent,
cmd = "cp -r {ontoref_dir}/templates/ontology {project_dir}/.ontology",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Ontology is required for gate checks before feature acceptance",
},
{
id = "init_kogral",
action = "initialize_kogral_graph",
actor = 'Agent,
cmd = "kogral init --name {project_name} --dir {project_dir}/.kogral",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
},
{
id = "create_syntaxis_project",
action = "register_project_in_syntaxis",
actor = 'Agent,
cmd = "syntaxis project create --name {project_name} --path {project_dir}",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
},
{
id = "define_nats_subjects",
action = "declare_service_nats_subjects",
actor = 'Both,
cmd = "^nats stream add {project_name} --subjects '{project_name}.>' --defaults err> /dev/null",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "NATS subjects must be defined before infra provisioning requests them",
},
{
id = "apply_infra",
action = "request_infrastructure_provisioning",
actor = 'Agent,
cmd = "nats pub ecosystem.provisioning.scaffold '{\"project_name\": \"{project_name}\", \"stack\": \"{stack}\"}'",
depends_on = [{ step = "define_nats_subjects", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "Publishes to NATS; provisioning service responds on ecosystem.provisioning.ready",
},
{
id = "register_in_ontoref",
action = "update_ontoref_project_registry",
actor = 'Human,
depends_on = [{ step = "apply_infra", kind = 'Always }],
on_error = { strategy = 'Continue },
note = "Human task: add the new service to ontoref CLAUDE.md related-projects table",
verify = "grep '{project_name}' {ontoref_dir}/.claude/CLAUDE.md",
},
{
id = "publish_ecosystem_event",
action = "announce_service_created",
actor = 'Agent,
cmd = "nats pub ecosystem.project.created '{\"project_name\": \"{project_name}\", \"stack\": \"{stack}\", \"type\": \"service\"}'",
depends_on = [
{ step = "register_in_ontoref", kind = 'Always },
{ step = "copy_ontology_template", kind = 'OnSuccess },
{ step = "init_kogral", kind = 'Always },
{ step = "create_syntaxis_project", kind = 'Always },
],
on_error = { strategy = 'Continue },
},
],
postconditions = [
"{project_dir} is an initialized git repository",
"{project_dir}/.ontology/ populated from templates",
"NATS stream '{project_name}' created (best-effort)",
"provisioning request published to ecosystem.provisioning.scaffold",
"ecosystem.project.created published",
],
} | (s.Mode String)

View File

@ -0,0 +1,111 @@
let s = import "../schema.ncl" in
# Mode: new_website
# Creates a new static website project using the evol-rustelo framework.
# Adds ontology, kogral, syntaxis, infra provisioning, DNS, and ecosystem announcement.
#
# Required params:
# {project_name} — site identifier
# {project_dir} — absolute path for the new site repository
# {ontoref_dir} — absolute path to the ontoref checkout
# {domain} — DNS domain to configure (e.g. "example.com")
# {infra_env} — target infra environment ("staging" | "production")
{
id = "new_website",
trigger = "Create a new evol-rustelo static website in the ecosystem",
preconditions = [
"git is available in PATH",
"nickel is available in PATH",
"cargo is available in PATH (evol-rustelo scaffold uses cargo generate)",
"{project_dir} parent directory exists and is writable",
"kogral CLI available (init_kogral continues on failure)",
"syntaxis CLI available (create_syntaxis_project continues on failure)",
"nats CLI available (best-effort steps continue on failure)",
"provisioning system reachable (apply_infra continues on failure)",
"{ontoref_dir}/templates/ontology/ exists",
],
steps = [
{
id = "init_repo",
action = "initialize_git_repository",
actor = 'Both,
cmd = "git -C {project_dir} init && git -C {project_dir} commit --allow-empty -m 'chore: initial commit'",
depends_on = [],
on_error = { strategy = 'Stop },
},
{
id = "copy_ontology_template",
action = "scaffold_ontology_directory",
actor = 'Agent,
cmd = "cp -r {ontoref_dir}/templates/ontology {project_dir}/.ontology",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
},
{
id = "init_kogral",
action = "initialize_kogral_graph",
actor = 'Agent,
cmd = "kogral init --name {project_name} --dir {project_dir}/.kogral",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
},
{
id = "create_syntaxis_project",
action = "register_project_in_syntaxis",
actor = 'Agent,
cmd = "syntaxis project create --name {project_name} --path {project_dir}",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
},
{
id = "scaffold_rustelo",
action = "generate_evol_rustelo_project",
actor = 'Agent,
cmd = "cargo generate --git https://repo.jesusperez.pro/jesus/evol-rustelo --name {project_name} --destination {project_dir}",
depends_on = [{ step = "init_repo", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Generates the evol-rustelo site scaffold with Nickel content schema",
},
{
id = "apply_infra",
action = "request_infrastructure_provisioning",
actor = 'Agent,
cmd = "nats pub ecosystem.provisioning.scaffold '{\"project_name\": \"{project_name}\", \"stack\": \"evol-rustelo\", \"env\": \"{infra_env}\"}'",
depends_on = [{ step = "scaffold_rustelo", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "Provisioning service responds on ecosystem.provisioning.ready with Nickel infra NCL path",
},
{
id = "configure_dns",
action = "point_domain_to_infra",
actor = 'Human,
depends_on = [{ step = "apply_infra", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Human task: configure {domain} DNS to point to provisioned infrastructure",
verify = "dig +short {domain} | grep -v '^$'",
},
{
id = "publish_ecosystem_event",
action = "announce_website_created",
actor = 'Agent,
cmd = "nats pub ecosystem.project.created '{\"project_name\": \"{project_name}\", \"domain\": \"{domain}\", \"type\": \"website\"}'",
depends_on = [
{ step = "configure_dns", kind = 'OnSuccess },
{ step = "copy_ontology_template", kind = 'OnSuccess },
{ step = "init_kogral", kind = 'Always },
{ step = "create_syntaxis_project", kind = 'Always },
],
on_error = { strategy = 'Continue },
},
],
postconditions = [
"{project_dir} is an initialized git repository with evol-rustelo scaffold",
"{project_dir}/.ontology/ populated from templates",
"infra provisioned for {infra_env} environment",
"{domain} DNS configured",
"ecosystem.project.created published",
],
} | (s.Mode String)

View File

@ -0,0 +1,67 @@
let d = import "../defaults.ncl" in
d.make_mode String {
id = "sync-ontology",
trigger = "Synchronize .ontology/ declarations with actual project artifacts — detect drift, propose patches, apply with confirmation",
preconditions = [
"ONTOREF_PROJECT_ROOT is set and points to a project with .ontology/core.ncl",
"nickel binary is available on PATH",
"Nushell >= 0.110.0 is available",
],
steps = [
{
id = "scan",
action = "Analyze project structure: crates, scenarios, agents, CI, forms, modes. If nightly toolchain is available, extract pub API surface via cargo doc JSON.",
cmd = "./ontoref sync scan",
actor = 'Both,
on_error = { strategy = 'Stop },
},
{
id = "diff",
action = "Compare scan results against .ontology/core.ncl nodes and edges. Categorize each item as OK, MISSING (artifact without node), STALE (node without artifact), DRIFT (node outdated), or BROKEN (edge referencing missing node).",
cmd = "./ontoref sync diff",
actor = 'Both,
depends_on = [{ step = "scan" }],
on_error = { strategy = 'Stop },
},
{
id = "propose",
action = "Generate Nickel code for new nodes (MISSING), mark stale nodes for removal, generate updated nodes for DRIFT, mark broken edges for deletion.",
cmd = "./ontoref sync propose",
actor = 'Both,
depends_on = [{ step = "diff" }],
on_error = { strategy = 'Stop },
},
{
id = "review",
action = "Human reviews the proposal. Agent can skip this step.",
actor = 'Human,
depends_on = [{ step = "propose" }],
on_error = { strategy = 'Stop },
},
{
id = "apply",
action = "Apply approved changes to .ontology/core.ncl. Each category (add/remove/update) confirmed separately.",
cmd = "./ontoref sync apply",
actor = 'Human,
depends_on = [{ step = "review" }],
on_error = { strategy = 'Stop },
},
{
id = "verify",
action = "Run nickel typecheck on modified files. Execute sync diff again to confirm zero drift.",
cmd = "nickel typecheck .ontology/core.ncl && ./ontoref sync diff",
actor = 'Both,
depends_on = [{ step = "apply" }],
on_error = { strategy = 'Stop },
},
],
postconditions = [
"nickel export .ontology/core.ncl succeeds without errors",
"sync diff reports zero MISSING, STALE, or BROKEN items",
"All existing ADR Hard constraints still pass",
],
}

View File

@ -0,0 +1,112 @@
let s = import "../schema.ncl" in
# Mode: typedialog-release
# Builds, packages, and announces a new typedialog release across all backends.
# Cross-compiles for Linux/macOS/Windows, generates SBOMs, creates distribution
# packages, runs the full CI pipeline, and publishes the ecosystem event.
#
# Required params:
# {version} — semver tag (e.g. "0.2.0")
# {typedialog_dir} — absolute path to the typedialog checkout
# {ontoref_dir} — absolute path to the ontoref checkout
{
id = "typedialog_release",
trigger = "Cut a new typedialog release: build, cross-compile, package, and announce",
preconditions = [
"git is available in PATH and working tree is clean",
"just is available in PATH",
"cargo is available (with cross or cross-compilation toolchains)",
"nickel is available for schema validation",
"nats CLI available (publish_ecosystem_event continues on failure)",
"{typedialog_dir}/justfile exists",
"GITHUB_TOKEN set (or Gitea token for Forgejo releases)",
"{version} matches semver pattern vMAJOR.MINOR.PATCH",
],
steps = [
{
id = "ci_full",
action = "run_full_ci_pipeline",
actor = 'Agent,
cmd = "just -f {typedialog_dir}/justfile ci::full",
depends_on = [],
on_error = { strategy = 'Stop },
note = "Runs fmt, clippy -D warnings, all tests, cargo audit, cargo deny — must pass before any release artifact is produced",
},
{
id = "build_release",
action = "build_release_binaries",
actor = 'Agent,
cmd = "just -f {typedialog_dir}/justfile build::release",
depends_on = [{ step = "ci_full", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
},
{
id = "cross_compile",
action = "cross_compile_all_targets",
actor = 'Agent,
cmd = "just -f {typedialog_dir}/justfile distro::cross",
depends_on = [{ step = "build_release", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "Cross-compiles for linux-x86_64, linux-aarch64, darwin-x86_64, darwin-aarch64, windows-x86_64; continues on partial failure",
},
{
id = "generate_sbom",
action = "generate_dependency_sbom",
actor = 'Agent,
cmd = "just -f {typedialog_dir}/justfile distro::generate-sbom",
depends_on = [{ step = "build_release", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "Generates SBOM.spdx.json (ISO/IEC 5962) and SBOM.cyclonedx.json (ECMA) for license compliance",
},
{
id = "create_package",
action = "create_distribution_package",
actor = 'Agent,
cmd = "just -f {typedialog_dir}/justfile distro::create-package",
depends_on = [
{ step = "cross_compile", kind = 'Always },
{ step = "generate_sbom", kind = 'Always },
],
on_error = { strategy = 'Stop },
note = "Packages all binaries (typedialog, typedialog-tui, typedialog-web, typedialog-ai, typedialog-ag, typedialog-prov-gen), configs, and install scripts",
},
{
id = "create_checksums",
action = "generate_release_checksums",
actor = 'Agent,
cmd = "just -f {typedialog_dir}/justfile distro::create-checksums",
depends_on = [{ step = "create_package", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
},
{
id = "tag_release",
action = "tag_git_release",
actor = 'Human,
depends_on = [{ step = "create_checksums", kind = 'OnSuccess }],
on_error = { strategy = 'Stop },
note = "Human task: git tag -s v{version} -m 'release: v{version}' && git push origin v{version}",
verify = "git -C {typedialog_dir} tag --list 'v{version}'",
},
{
id = "publish_ecosystem_event",
action = "announce_release_on_nats",
actor = 'Agent,
cmd = "nats pub ecosystem.typedialog.release '{\"version\": \"{version}\", \"backends\": [\"cli\", \"tui\", \"web\", \"ai\", \"agent\", \"prov-gen\"]}'",
depends_on = [{ step = "tag_release", kind = 'OnSuccess }],
on_error = { strategy = 'Continue },
note = "Announces the release to the ecosystem; stratum-orchestrator may trigger dependent workflows (e.g. provisioning schema updates)",
},
],
postconditions = [
"Full CI pipeline passes for commit at {version}",
"Release artifacts in {typedialog_dir}/target/release/ and distro packages",
"SBOM.spdx.json and SBOM.cyclonedx.json regenerated",
"Distribution package contains all 6 backend binaries and install scripts",
"SHA256 checksums file created alongside the package",
"git tag v{version} pushed to origin",
"ecosystem.typedialog.release published to NATS (best-effort)",
],
} | (s.Mode String)

303
reflection/modules/adr.nu Normal file
View File

@ -0,0 +1,303 @@
#!/usr/bin/env nu
# reflection/modules/adr.nu — ADR management commands.
#
# Error handling policy:
# adr list / constraints — graceful skip via do { ^nickel export } | complete
# (draft ADRs may be incomplete; list must always work)
# adr validate — ^nickel export | complete for fresh data (no plugin cache)
# adr show — fail loud (caller passes a known valid id)
use env.nu *
use store.nu [daemon-export, daemon-export-safe]
# Formats an ADR record as readable markdown text.
def adr-to-md []: record -> string {
let a = $in
mut lines = []
$lines = ($lines | append $"# ($a.id): ($a.title)")
$lines = ($lines | append $"")
$lines = ($lines | append $"**Status**: ($a.status) **Date**: ($a.date)")
$lines = ($lines | append $"")
$lines = ($lines | append "## Context")
$lines = ($lines | append $a.context)
$lines = ($lines | append $"")
$lines = ($lines | append "## Decision")
$lines = ($lines | append $a.decision)
$lines = ($lines | append $"")
$lines = ($lines | append "## Rationale")
for r in $a.rationale {
$lines = ($lines | append $"- **($r.claim)**")
$lines = ($lines | append $" ($r.detail)")
}
$lines = ($lines | append $"")
$lines = ($lines | append "## Consequences")
$lines = ($lines | append "**Positive**")
for p in $a.consequences.positive {
$lines = ($lines | append $"- ($p)")
}
$lines = ($lines | append "**Negative**")
for n in $a.consequences.negative {
$lines = ($lines | append $"- ($n)")
}
$lines = ($lines | append $"")
$lines = ($lines | append "## Alternatives Considered")
for alt in $a.alternatives_considered {
$lines = ($lines | append $"- **($alt.option)**: ($alt.why_rejected)")
}
$lines = ($lines | append $"")
$lines = ($lines | append "## Constraints")
for c in $a.constraints {
$lines = ($lines | append $"### ($c.id) [($c.severity)]")
$lines = ($lines | append $c.claim)
$lines = ($lines | append $"- Scope: ($c.scope)")
$lines = ($lines | append $"- Check: `($c.check_hint)`")
$lines = ($lines | append $"- Rationale: ($c.rationale)")
$lines = ($lines | append $"")
}
let oc = $a.ontology_check
$lines = ($lines | append "## Ontology Check")
$lines = ($lines | append $"- Verdict: **($oc.verdict)**")
if ($oc.invariants_at_risk | is-not-empty) {
$lines = ($lines | append $"- Invariants at risk: ($oc.invariants_at_risk | str join ', ')")
}
if ($a.related_adrs | is-not-empty) {
$lines = ($lines | append $"")
$lines = ($lines | append $"## Related ADRs")
$lines = ($lines | append ($a.related_adrs | str join ", "))
}
$lines | str join "\n"
}
# Resolve effective format: explicit flag wins; otherwise md for humans, json for agents.
def resolve-fmt [fmt: string, human_default: string]: nothing -> string {
if ($fmt | is-not-empty) { return $fmt }
let actor = ($env.ONTOREF_ACTOR? | default "developer")
if $actor == "agent" { "json" } else { $human_default }
}
# Serialize or render a value in the requested format.
# fmt: "md" (default readable markdown) | "table" (Nu expand) | "json" | "yaml" | "toml"
def fmt-output [fmt: string]: any -> any {
let data = $in
match $fmt {
"json" => { $data | to json },
"yaml" => { $data | to yaml },
"toml" => {
# to toml requires a record at the top level — wrap lists
if (($data | describe) =~ "^(list|table)") {
{ items: $data } | to toml
} else {
$data | to toml
}
},
"table" => { $data | table --expand },
_ => {
if (($data | describe) =~ "^record") {
$data | adr-to-md
} else {
# list/table — render as markdown table
$data | to md
}
},
}
}
# Print a value — needed inside while loops where return values are discarded.
def print-output [fmt: string]: any -> nothing {
let data = $in
match $fmt {
"json" | "yaml" | "toml" => { print $data },
"table" => { print ($data | table --expand) },
_ => {
if (($data | describe) =~ "^record") {
print ($data | adr-to-md)
} else {
print ($data | to md)
}
},
}
}
export def "adr list" [
--fmt: string = "", # Output format: table (human) | json (agent) | yaml | toml
] {
let f = (resolve-fmt $fmt "table")
adr-files | each { |ncl|
let data = (daemon-export-safe $ncl)
if $data != null { $data | select id title status date } else { null }
} | compact | sort-by date | fmt-output $f
}
export def "adr validate" [] {
let hard = (
adr-files | each { |ncl|
daemon-export-safe $ncl
}
| compact
| where status == "Accepted"
| get constraints
| flatten
| where severity == "Hard"
)
let count = ($hard | length)
print $"Running ($count) Hard constraints..."
print ""
let results = $hard | each { |c|
let result = do { nu -c $c.check_hint } | complete
{
id: $c.id,
claim: $c.claim,
passed: ($result.exit_code == 0 and ($result.stdout | str trim | is-empty)),
output: ($result.stdout | str trim),
}
}
for r in $results {
let icon = if $r.passed { "✓" } else { "✗" }
print $"($icon) ($r.id): ($r.claim)"
if not $r.passed and ($r.output | is-not-empty) {
print $" Violation: ($r.output)"
}
}
let failures = $results | where passed == false
if ($failures | is-not-empty) {
let n = ($failures | length)
error make { msg: $"($n) Hard constraints violated" }
}
}
export def "adr show" [
id?: string, # ADR id: "001", "adr-001", or "adr-001-slug"
--interactive (-i), # Select from list interactively via typedialog
--fmt: string = "", # Output format: md (human) | json (agent) | table | yaml | toml
] {
let f = (resolve-fmt $fmt "md")
if $interactive or ($id | is-empty) {
adr-show-interactive $f
} else {
adr-show-by-id $id $f
}
}
export def "constraints" [
--fmt: string = "", # Output format: table (human) | json (agent) | yaml | toml
] {
let f = (resolve-fmt $fmt "table")
adr-files | each { |ncl|
daemon-export-safe $ncl
}
| compact
| where status == "Accepted"
| each { |a| $a.constraints | each { |c| $c | merge { adr: $a.id } } }
| flatten
| where severity == "Hard"
| select adr id claim check_hint
| fmt-output $f
}
export def "adr help" [] {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let cmd = ($env.ONTOREF_CALLER? | default "./onref")
print ""
print "ADR commands:"
print $" ($cmd) adr list list all ADRs with status"
print $" ($cmd) adr list --fmt json output as json/yaml/toml"
print $" ($cmd) adr validate run Hard constraint checks"
print $" ($cmd) adr show interactive ADR browser (no args)"
print $" ($cmd) adr show <id> show a specific ADR"
print $" ($cmd) constraints show active Hard constraints"
if $actor == "agent" {
print ""
print "Agent query pattern:"
print " nickel-export adrs/adr-NNN-name.ncl"
print " nickel-export adrs/adr-NNN-name.ncl | get constraints | where severity == 'Hard'"
}
print ""
}
# ── Internal ───────────────────────────────────────────────────────────────────
export def "adr accept" [
id: string, # adr-NNN or NNN
] {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let canonical = if ($id | str starts-with "adr-") { $id } else { $"adr-($id)" }
let files = glob $"($root)/adrs/($canonical)-*.ncl"
if ($files | is-empty) {
error make { msg: $"ADR '($id)' not found in adrs/" }
}
let path = ($files | first)
let content = open $path
if not ($content | str contains "'Proposed") {
error make { msg: $"ADR '($id)' is not in Proposed status — current file does not contain \"'Proposed\"" }
}
$content | str replace --all "'Proposed" "'Accepted" | save --force $path
print $" accepted: ($path | path basename)"
do { ^nickel typecheck $path } | complete
| if $in.exit_code != 0 {
error make { msg: $"typecheck failed after accept:\n($in.stderr)" }
} else {
print " typecheck: ok"
}
}
def adr-files [] {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
glob $"($root)/adrs/adr-*.ncl"
}
def adr-show-by-id [id: string, fmt: string] {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let canonical = if ($id | str starts-with "adr-") { $id } else { $"adr-($id)" }
let files = glob $"($root)/adrs/($canonical)-*.ncl"
if ($files | is-empty) {
error make { msg: $"ADR '($id)' not found in adrs/" }
}
let data = (daemon-export ($files | first))
$data | fmt-output $fmt
}
def adr-show-interactive [fmt: string] {
let entries = (
adr-files | each { |ncl|
let data = (daemon-export-safe $ncl)
if $data != null { $data | select id title status } else { null }
} | compact | sort-by id
)
if ($entries | is-empty) {
print "No ADRs found."
return
}
let options = ($entries | each { |a| $"($a.id) ($a.title) [($a.status)]" })
let menu = ($options | append "— Exit —")
mut browsing = true
while $browsing {
let choice = (typedialog select "Select ADR:" $menu)
if $choice == "— Exit —" {
$browsing = false
} else {
let idx = ($options | enumerate | where { |r| $r.item == $choice } | first).index
let selected = ($entries | get $idx)
print ""
adr-show-by-id $selected.id $fmt | print-output $fmt
print ""
let next = (typedialog select "Continue?" ["Browse" "Exit"])
if $next == "Exit" { $browsing = false }
}
}
}

View File

@ -0,0 +1,233 @@
#!/usr/bin/env nu
# reflection/modules/backlog.nu — backlog management commands.
use env.nu *
use store.nu [daemon-export-safe]
export def "backlog list" [
--status: string = "",
--kind: string = "",
--fmt: string = "",
] {
let f = if ($fmt | is-not-empty) { $fmt } else { "table" }
let items = (backlog-load)
let rows = if ($status | is-not-empty) {
$items | where status == $status
} else if ($kind | is-not-empty) {
$items | where kind == $kind
} else {
$items | where status != "Done" | where status != "Cancelled"
}
let display = $rows | select id title kind priority status | sort-by priority
match $f {
"json" => { $display | to json },
"md" => { $display | to md },
_ => { $display | table },
}
}
export def "backlog show" [id: string] {
let item = (backlog-load) | where id == $id | first
print ""
print $"[($item.status)] ($item.id) — ($item.title)"
print $"Kind: ($item.kind) Priority: ($item.priority)"
if ($item.detail? | default "" | str length) > 0 { print ""; print $item.detail }
let adrs = ($item.related_adrs? | default [])
let modes = ($item.related_modes? | default [])
if ($adrs | is-not-empty) { print $"ADRs: ($adrs | str join ', ')" }
if ($modes | is-not-empty) { print $"Modes: ($modes | str join ', ')" }
if ($item.related_dim? | is-not-empty) { print $"Dimension: ($item.related_dim)" }
if ($item.graduates_to? | is-not-empty) { print $"Graduates to: ($item.graduates_to)" }
print ""
}
export def "backlog add" [
title: string,
--kind: string = "Todo",
--priority: string = "Medium",
--detail: string = "",
--dim: string = "",
--adr: string = "",
--mode: string = "",
] {
let root = (backlog-root)
let file = $"($root)/reflection/backlog.ncl"
if not ($file | path exists) {
print $"No backlog.ncl at ($file)"
return
}
let items = (backlog-load)
let next_num = if ($items | is-empty) { 1 } else {
($items | each { |i| $i.id | str replace "bl-" "" | into int } | sort | last) + 1
}
let new_id = $"bl-($next_num | fill --alignment right --width 3 --character '0')"
let today = (date now | format date "%Y-%m-%d")
let adrs_list = if ($adr | str length) > 0 { [$adr] } else { [] }
let modes_list = if ($mode | str length) > 0 { [$mode] } else { [] }
print ""
print $"New backlog item: ($new_id)"
print $" title = ($title)"
print $" kind = ($kind)"
print $" priority = ($priority)"
print ""
print $"Add to reflection/backlog.ncl:"
print $" \{"
print $" id = \"($new_id)\","
print $" title = \"($title)\","
print $" kind = '($kind),"
print $" priority = '($priority),"
print $" status = 'Open,"
print $" detail = \"($detail)\","
print $" created = \"($today)\","
print $" updated = \"($today)\","
print $" \},"
}
export def "backlog done" [id: string] {
backlog-set-status $id "Done"
}
export def "backlog cancel" [id: string] {
backlog-set-status $id "Cancelled"
}
export def "backlog promote" [id: string] {
let item = (backlog-load) | where id == $id | first
print ""
print $"Promoting ($item.id): ($item.title)"
print $"Graduates to: ($item.graduates_to? | default 'unset')"
print ""
let target = ($item.graduates_to? | default "")
if $target == "Adr" {
let flag = if ($item.priority == "Critical" or $item.priority == "High") { "-a " } else { "" }
print "Run in a Claude session:"
print $" /create-adr ($flag)\"($item.title)\""
} else if $target == "Mode" {
let mode_id = ($item.title | str downcase | str replace --all " " "-" | str replace --all --regex '[^a-z0-9-]' "")
print $" ontoref register → affects_capability=true, mode_id=($mode_id)"
} else if $target == "StateTransition" {
let dim = ($item.related_dim? | default "")
print " ontoref register → changes_ontology_state=true"
if ($dim | str length) > 0 { print $" dimension_id: ($dim)" }
} else if $target == "PrItem" {
print " ontoref mode create-pr"
} else {
print " No graduation target set. Edit backlog.ncl to add graduates_to."
}
print ""
}
export def "backlog roadmap" [] {
let root = (backlog-root)
let bl = (backlog-load) | where status != "Done" | where status != "Cancelled"
let state_file = $"($root)/.ontology/state.ncl"
let dims = if ($state_file | path exists) {
let data = (daemon-export-safe $state_file)
if $data != null { $data | get dimensions } else { [] }
} else { [] }
print ""
print "═══ ROADMAP ═══════════════════════════════════════"
if ($dims | is-not-empty) {
print ""
print "STATE DIMENSIONS"
for d in $dims {
if $d.current_state != $d.desired_state {
print $" ($d.id) ($d.current_state) → ($d.desired_state) [($d.horizon)]"
}
}
}
for p in ["Critical", "High", "Medium", "Low"] {
let p_items = $bl | where priority == $p
if ($p_items | is-not-empty) {
print ""
print ($p | str upcase)
for i in $p_items {
let tag = if $i.status == "InProgress" { "[~]" } else { "[ ]" }
print $" ($tag) ($i.id) ($i.title) [($i.kind)]"
}
}
}
print ""
print "═══════════════════════════════════════════════════"
print ""
}
# ── Internal ────────────────────────────────────────────────────────────────────
def backlog-root []: nothing -> string {
$env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT
}
def backlog-load [] {
let file = $"($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)/reflection/backlog.ncl"
if not ($file | path exists) { return [] }
let data = (daemon-export-safe $file)
if $data == null { return [] }
$data | get items
}
def backlog-set-status [id: string, new_status: string] {
let file = $"($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)/reflection/backlog.ncl"
let today = (date now | format date "%Y-%m-%d")
let lines = (open --raw $file | lines)
# Find the line index of the id field for this item
let id_pattern = $"\"($id)\""
let id_line = ($lines | enumerate | where { |r| $r.item | str contains $id_pattern } | first)
if ($id_line | is-empty) {
print $" error: id '($id)' not found in ($file)"
return
}
let start = $id_line.index
# Scan forward from the id line to find the closing `},` of this item record.
# Track brace depth: the id line is already inside the outer record (depth=1 going in).
mut depth = 0
mut end_line = $start
for i in ($start..($lines | length | $in - 1)) {
let l = ($lines | get $i)
let opens = ($l | split chars | where $it == "{" | length)
let closes = ($l | split chars | where $it == "}" | length)
$depth = $depth + $opens - $closes
if $depth < 0 {
$end_line = $i
break
}
}
# Bind end_line to immutable before closure (Nu forbids capturing mut vars in closures)
let end_idx = $end_line
# Patch status and updated within the located range [start, end_idx]
let patched = ($lines | enumerate | each { |r|
if $r.index >= $start and $r.index <= $end_idx {
$r.item
| str replace --regex "status[[:space:]]*=[[:space:]]*'[A-Za-z]+" $"status = '($new_status)"
| str replace --regex "updated[[:space:]]*=[[:space:]]*\"[^\"]*\"" $"updated = \"($today)\""
} else {
$r.item
}
} | str join "\n")
$patched | save --force $file
let tc = do { ^nickel typecheck $file } | complete
if $tc.exit_code != 0 {
print " error: typecheck failed — reverting"
do { ^git checkout -- $file } | complete | ignore
return
}
print $" ($id) → ($new_status)"
}

657
reflection/modules/coder.nu Normal file
View File

@ -0,0 +1,657 @@
# .coder/ management — author workspaces, inbox triage, insight graduation
#
# Structure:
# .coder/<author>/author.ncl — identity (validated)
# .coder/<author>/inbox/ — dump zone, zero ceremony
# .coder/<author>/<category>/ — classified content
# .coder/<author>/<category>/context.ncl — category metadata (validated)
# .coder/general/<category>/ — published from all authors
#
# File naming: {description}.{kind}.md where kind ∈ {done,plan,info,review,audit,commit}
# Legacy files with _ separator are accepted and normalized during triage.
use store.nu [daemon-export-safe]
const VALID_KINDS = ["done", "plan", "info", "review", "audit", "commit"]
def coder-root []: nothing -> string {
let root = if ($env.ONTOREF_PROJECT_ROOT? | is-not-empty) {
$env.ONTOREF_PROJECT_ROOT
} else {
$env.PWD
}
$"($root)/.coder"
}
# Extract kind from filename, handling both . and _ separators
def extract-kind [filename: string]: nothing -> string {
let base = ($filename | str replace '.md' '')
for kind in $VALID_KINDS {
if ($base | str ends-with $".($kind)") or ($base | str ends-with $"_($kind)") {
return $kind
}
}
""
}
# Map file kind to triage category
def kind-to-category [kind: string]: nothing -> string {
match $kind {
"done" => "features"
"plan" => "features"
"info" => "investigations"
"review" => "reviews"
"audit" => "reviews"
"commit" => "features"
_ => ""
}
}
# Classify by content keywords (fallback when kind is empty)
def classify-by-content [filepath: string]: nothing -> string {
let first_lines = (open $filepath | lines | first 5 | str join " " | str downcase)
if ($first_lines =~ "insight|til |learned|pattern|trap|gotcha") {
"insights"
} else if ($first_lines =~ "fix|bug|error|broken|crash|hotfix") {
"bugfixes"
} else if ($first_lines =~ "decision|chose|adr|rejected|alternative") {
"decisions"
} else if ($first_lines =~ "plan|implement|design|proposal") {
"features"
} else {
""
}
}
# Extract title from markdown first heading or filename
def extract-title [filepath: string]: nothing -> string {
let lines = (open $filepath | lines)
let heading = ($lines | where { $in =~ '^#+ ' } | first | default "")
if ($heading | is-not-empty) {
$heading | str replace -r '^#+\s*' '' | str trim
} else {
# Fall back to filename without extension and kind suffix
mut base = ($filepath | path basename | str replace '.md' '')
for kind in $VALID_KINDS {
$base = ($base | str replace $".($kind)" '' | str replace $"_($kind)" '')
}
$base | str replace -a '_' ' ' | str replace -a '-' ' '
}
}
# Extract date from filename (YYYY-MM-DD or YYYYMMDD prefix).
# Nushell str substring ranges are inclusive on both ends.
def extract-date [filename: string]: nothing -> string {
if ($filename =~ '^\d{4}-\d{2}-\d{2}') {
$filename | str substring 0..9
} else if ($filename =~ '^\d{8}') {
let raw = ($filename | str substring 0..7)
let y = ($raw | str substring 0..3)
let m = ($raw | str substring 4..5)
let d = ($raw | str substring 6..7)
$"($y)-($m)-($d)"
} else {
""
}
}
# Extract keyword tags from content
def extract-tags [filepath: string]: nothing -> list<string> {
let content = (open $filepath | str downcase)
let tag_map = [
["nickel", "nickel"], ["nushell", "nushell"], ["rust", "rust"],
["cargo", "cargo"], ["bacon", "bacon"], ["ci", "ci"],
["encryption", "encryption"], ["ontology", "ontology"],
["surrealdb", "surrealdb"], ["nats", "nats"],
["agent", "agent"], ["llm", "llm"], ["docker", "docker"],
]
mut tags = []
for pair in $tag_map {
let keyword = ($pair | get 0)
let tag = ($pair | get 1)
if ($content =~ $keyword) {
$tags = ($tags | append $tag)
}
}
$tags
}
# Map category string to NCL enum value
def category-to-ncl [cat: string]: nothing -> string {
match $cat {
"insights" => "'Insight"
"features" => "'Feature"
"bugfixes" => "'Bugfix"
"investigations" => "'Investigation"
"decisions" => "'Decision"
"reviews" => "'Review"
"resources" => "'Resource"
_ => "'Inbox"
}
}
# Generate companion NCL for a classified file
def generate-companion-ncl [
filepath: string
author: string
kind: string
category: string
]: nothing -> string {
let title = (extract-title $filepath)
let date = (extract-date ($filepath | path basename))
let tags = (extract-tags $filepath)
let kind_ncl = if ($kind | is-empty) { "'unknown" } else { $"'($kind)" }
let cat_ncl = (category-to-ncl $category)
let tags_ncl = ($tags | each {|t| $"\"($t)\""} | str join ", ")
let safe_title = ($title | str replace -a '"' '\\"')
let title_line = $' title = "($safe_title)",'
let author_line = $' author = "($author)",'
let kind_line = $' kind = ($kind_ncl),'
let cat_line = $' category = ($cat_ncl),'
let date_line = if ($date | is-not-empty) { $' date = "($date)",' } else { "" }
let tags_line = if ($tags | is-not-empty) { $' tags = [($tags_ncl)],' } else { "" }
([
'let d = import "coder-defaults.ncl" in'
'let c = import "coder-constraints.ncl" in'
''
'(d.make_entry {'
$title_line
$date_line
$author_line
$kind_line
$cat_line
$tags_line
'}) | c.NonEmptyTitle | c.NonEmptyAuthor'
] | where {|line| ($line | str length) > 0 } | str join "\n")
}
# List all authors in .coder/
export def "coder authors" []: nothing -> table {
let root = (coder-root)
if not ($root | path exists) { return [] }
ls $root
| where type == "dir"
| where { ($in.name | path basename) != "general" and ($in.name | path basename) != "insights" }
| each {|d|
let author_ncl = $"($d.name)/author.ncl"
let name = ($d.name | path basename)
let has_meta = ($author_ncl | path exists)
{ name: $name, has_metadata: $has_meta, path: $d.name }
}
}
# Initialize an author workspace
export def "coder init" [
author: string
--actor: string = "Human"
--model: string = ""
]: nothing -> string {
let root = (coder-root)
let author_dir = $"($root)/($author)"
let inbox_dir = $"($author_dir)/inbox"
if ($author_dir | path exists) {
return $"author '($author)' already exists at ($author_dir)"
}
mkdir $inbox_dir
let actor_value = match $actor {
"Human" => "'Human"
"AgentClaude" => "'AgentClaude"
"AgentCustom" => "'AgentCustom"
"CI" => "'CI"
_ => "'Human"
}
let model_line = if ($model | is-empty) { "" } else { $" model = \"($model)\"," }
let ncl_content = ([
'let s = import "coder.ncl" in'
''
's.Author & {'
$" name = \"($author)\","
$" actor = ($actor_value),"
$model_line
'}'
] | where {|line| ($line | str length) > 0 } | str join "\n")
$ncl_content | save --force $"($author_dir)/author.ncl"
$"initialized ($author_dir) with inbox/"
}
# Triage: classify files in an author's inbox/ into categories
export def "coder triage" [
author: string
--dry-run (-n)
--interactive (-i)
]: nothing -> table {
let root = (coder-root)
let inbox_dir = $"($root)/($author)/inbox"
if not ($inbox_dir | path exists) {
print $"no inbox/ at ($inbox_dir)"
return []
}
let files = (ls $inbox_dir | where type == "file")
if ($files | is-empty) {
print "inbox/ is empty"
return []
}
mut results = []
for file in $files {
let filename = ($file.name | path basename)
let kind = (extract-kind $filename)
let by_kind = (kind-to-category $kind)
let by_content = (classify-by-content $file.name)
let category = if ($by_kind | is-not-empty) {
$by_kind
} else if ($by_content | is-not-empty) {
$by_content
} else {
"unclassified"
}
let target_dir = $"($root)/($author)/($category)"
if $category == "unclassified" {
$results = ($results | append { file: $filename, kind: $kind, category: $category, action: "skip", ncl: "" })
} else if $dry_run {
$results = ($results | append { file: $filename, kind: $kind, category: $category, action: "would move", ncl: "" })
} else if $interactive {
let answer = (input $"($filename) → ($category)? [y/n/c] (c=change category) ")
if ($answer | str starts-with "y") {
let ncl = (generate-companion-ncl $file.name $author $kind $category)
let ncl_name = ($filename | str replace '.md' '.ncl')
mkdir $target_dir
mv $file.name $"($target_dir)/($filename)"
$ncl | save --force $"($target_dir)/($ncl_name)"
$results = ($results | append { file: $filename, kind: $kind, category: $category, action: "moved", ncl: $ncl_name })
} else if ($answer | str starts-with "c") {
let new_cat = (input "category: ")
let ncl = (generate-companion-ncl $file.name $author $kind $new_cat)
let ncl_name = ($filename | str replace '.md' '.ncl')
let new_dir = $"($root)/($author)/($new_cat)"
mkdir $new_dir
mv $file.name $"($new_dir)/($filename)"
$ncl | save --force $"($new_dir)/($ncl_name)"
$results = ($results | append { file: $filename, kind: $kind, category: $new_cat, action: "moved", ncl: $ncl_name })
} else {
$results = ($results | append { file: $filename, kind: $kind, category: $category, action: "skipped", ncl: "" })
}
} else {
let ncl = (generate-companion-ncl $file.name $author $kind $category)
let ncl_name = ($filename | str replace '.md' '.ncl')
mkdir $target_dir
mv $file.name $"($target_dir)/($filename)"
$ncl | save --force $"($target_dir)/($ncl_name)"
$results = ($results | append { file: $filename, kind: $kind, category: $category, action: "moved", ncl: $ncl_name })
}
}
$results
}
# Publish: promote files from author workspace to .coder/general/
# Renames files to include author: {author}-{original-name}
export def "coder publish" [
author: string
category: string
--dry-run (-n)
--all (-a) # publish all files in category (default: interactive pick)
]: nothing -> table {
let root = (coder-root)
let source_dir = $"($root)/($author)/($category)"
if not ($source_dir | path exists) {
print $"no ($category)/ for author ($author)"
return []
}
let general_dir = $"($root)/general/($category)"
let files = (ls $source_dir
| where type == "file"
| where { ($in.name | path basename) != "context.ncl" })
if ($files | is-empty) {
print $"no files in ($category)/"
return []
}
mut results = []
for file in $files {
let fname = ($file.name | path basename)
let target_name = $"($author)-($fname)"
if (not $all) and (not $dry_run) {
let answer = (input $"publish ($fname) as ($target_name)? [y/n] ")
if not ($answer | str starts-with "y") {
$results = ($results | append { file: $fname, target: $target_name, action: "skipped" })
continue
}
}
if $dry_run {
$results = ($results | append { file: $fname, target: $target_name, action: "would publish" })
} else {
mkdir $general_dir
cp $file.name $"($general_dir)/($target_name)"
$results = ($results | append { file: $fname, target: $target_name, action: "published" })
}
}
$results
}
# List contents of an author workspace or general, grouped by category
export def "coder ls" [
author: string = ""
--category (-c): string = ""
]: nothing -> table {
let root = (coder-root)
if not ($root | path exists) { return [] }
let search_dir = if ($author | is-empty) {
$"($root)/general"
} else {
$"($root)/($author)"
}
if not ($search_dir | path exists) {
print $"directory not found: ($search_dir)"
return []
}
let dirs = (ls $search_dir | where type == "dir")
mut items = []
for dir in $dirs {
let cat = ($dir.name | path basename)
if ($category | is-not-empty) and ($cat != $category) { continue }
let files = (ls $dir.name
| where type == "file"
| where { let b = ($in.name | path basename); $b != "context.ncl" and $b != "author.ncl" })
for file in $files {
let fname = ($file.name | path basename)
let kind = (extract-kind $fname)
let date = if ($fname =~ '^\d{4}-\d{2}-\d{2}') or ($fname =~ '^\d{8}') {
$fname | str substring 0..9
} else {
""
}
$items = ($items | append {
category: $cat,
file: $fname,
kind: $kind,
date: $date,
modified: $file.modified,
})
}
}
$items | sort-by modified --reverse
}
# Search across all authors by content pattern
export def "coder search" [
pattern: string
--author (-a): string = ""
]: nothing -> table {
let root = (coder-root)
if not ($root | path exists) { return [] }
let search_path = if ($author | is-not-empty) {
$"($root)/($author)"
} else {
$root
}
let md_files = (glob $"($search_path)/**/*.md")
mut matches = []
for file in $md_files {
let content = (open $file | default "")
if ($content =~ $pattern) {
let rel = ($file | str replace $"($root)/" "")
let parts = ($rel | split row "/")
let file_author = if ($parts | length) > 1 { $parts | first } else { "root" }
let fname = ($parts | last)
$matches = ($matches | append { author: $file_author, file: $fname, path: $rel })
}
}
$matches
}
# ── JSON record commands ──────────────────────────────────────────────────────
# Structured entries stored as JSONL (one JSON object per line).
# Machine-readable alternative to markdown — no companion NCL needed.
# Storage: .coder/<author>/<category>/entries.jsonl
const VALID_CATEGORIES = ["insights", "features", "bugfixes", "investigations", "decisions", "reviews", "resources"]
const VALID_DOMAINS = ["Language", "Runtime", "Architecture", "Tooling", "Pattern", "Debugging", "Security", "Performance"]
# Record a structured JSON entry into a category's JSONL log
export def "coder record" [
author: string
--kind (-k): string = "info"
--category (-c): string = ""
--title (-t): string
--tags: list<string> = []
--relates_to: list<string> = []
--trigger: string = ""
--files_touched: list<string> = []
--domain (-d): string = ""
--reusable (-r)
content: string
]: nothing -> record {
let root = (coder-root)
let author_dir = $"($root)/($author)"
if not ($author_dir | path exists) {
error make { msg: $"author '($author)' not initialized — run: coder init ($author)" }
}
if ($title | str trim | is-empty) {
error make { msg: "title must not be empty" }
}
if ($kind | is-not-empty) and (not ($kind in $VALID_KINDS)) {
error make { msg: $"invalid kind '($kind)' — valid: ($VALID_KINDS | str join ', ')" }
}
let resolved_category = if ($category | is-not-empty) {
$category
} else {
let by_kind = (kind-to-category $kind)
if ($by_kind | is-not-empty) { $by_kind } else { "inbox" }
}
if ($resolved_category != "inbox") and (not ($resolved_category in $VALID_CATEGORIES)) {
error make { msg: $"invalid category '($resolved_category)' — valid: ($VALID_CATEGORIES | str join ', ')" }
}
let target_dir = $"($author_dir)/($resolved_category)"
mkdir $target_dir
let now = (date now | format date "%Y-%m-%d")
mut entry = {
title: $title,
kind: $kind,
category: $resolved_category,
author: $author,
date: $now,
tags: $tags,
relates_to: $relates_to,
content: $content,
}
if ($trigger | is-not-empty) or ($files_touched | is-not-empty) {
$entry = ($entry | merge {
context: {
trigger: $trigger,
files_touched: $files_touched,
}
})
}
if ($domain | is-not-empty) {
if not ($domain in $VALID_DOMAINS) {
error make { msg: $"invalid domain '($domain)' — valid: ($VALID_DOMAINS | str join ', ')" }
}
$entry = ($entry | merge { domain: $domain, reusable: $reusable })
}
let jsonl_path = $"($target_dir)/entries.jsonl"
let json_line = ($entry | to json --raw)
$"($json_line)\n" | save --raw --append $jsonl_path
$entry
}
# Query JSONL entries across categories/authors with optional filters
export def "coder log" [
--author (-a): string = ""
--category (-c): string = ""
--tag (-t): string = ""
--kind (-k): string = ""
--domain (-d): string = ""
--limit (-l): int = 0
--json (-j)
]: nothing -> table {
let root = (coder-root)
if not ($root | path exists) { return [] }
let pattern = if ($author | is-not-empty) and ($category | is-not-empty) {
$"($root)/($author)/($category)/entries.jsonl"
} else if ($author | is-not-empty) {
$"($root)/($author)/**/entries.jsonl"
} else if ($category | is-not-empty) {
$"($root)/**/($category)/entries.jsonl"
} else {
$"($root)/**/entries.jsonl"
}
let files = (glob $pattern)
if ($files | is-empty) { return [] }
mut entries = []
for file in $files {
let lines = (open $file | lines | where { $in | str trim | is-not-empty })
for line in $lines {
# Guard: only attempt JSON parse on lines that look like objects.
# from json is an internal command — errors cannot be captured with | complete.
let trimmed = ($line | str trim)
if ($trimmed | str starts-with "{") and ($trimmed | str ends-with "}") {
let parsed = ($trimmed | from json)
if ($parsed | describe | str starts-with "record") {
$entries = ($entries | append $parsed)
}
}
}
}
if ($tag | is-not-empty) {
$entries = ($entries | where { ($in.tags? | default []) | any {|t| $t == $tag } })
}
if ($kind | is-not-empty) {
$entries = ($entries | where { ($in.kind? | default "") == $kind })
}
if ($domain | is-not-empty) {
$entries = ($entries | where { ($in.domain? | default "") == $domain })
}
let sorted = ($entries | sort-by date --reverse)
if $limit > 0 {
$sorted | first $limit
} else {
$sorted
}
}
# Export all JSONL entries as a single JSON array (for external tools/databases)
export def "coder export" [
--author (-a): string = ""
--category (-c): string = ""
--format (-f): string = "json"
]: nothing -> string {
let entries = (coder log --author $author --category $category)
match $format {
"json" => { $entries | to json }
"jsonl" => { $entries | each { to json --raw } | str join "\n" }
"csv" => {
$entries
| select title kind category author date tags
| update tags { str join ";" }
| to csv
}
_ => { error make { msg: $"unsupported format '($format)' — use json, jsonl, or csv" } }
}
}
# Graduate: copy items to committed project path (e.g. reflection/knowledge/)
export def "coder graduate" [
source_category: string # "general/insights" or "jesus/insights"
--target (-t): string = "reflection/knowledge"
--dry-run (-n)
]: nothing -> table {
let root = (coder-root)
let source_dir = $"($root)/($source_category)"
if not ($source_dir | path exists) {
print $"not found: ($source_dir)"
return []
}
# Check if category is graduable
let context_path = $"($source_dir)/context.ncl"
if ($context_path | path exists) {
let ctx = (daemon-export-safe $context_path)
if ($ctx != null) {
if not ($ctx.graduable? | default false) {
print $"'($source_category)' is not marked graduable in context.ncl"
return []
}
}
}
let project_root = if ($env.ONTOREF_PROJECT_ROOT? | is-not-empty) {
$env.ONTOREF_PROJECT_ROOT
} else {
$env.PWD
}
let target_dir = $"($project_root)/($target)"
let files = (ls $source_dir
| where type == "file"
| where { ($in.name | path basename) != "context.ncl" })
mut results = []
for file in $files {
let fname = ($file.name | path basename)
if $dry_run {
$results = ($results | append { file: $fname, action: "would graduate", target: $target_dir })
} else {
mkdir $target_dir
cp $file.name $"($target_dir)/($fname)"
$results = ($results | append { file: $fname, action: "graduated", target: $target_dir })
}
}
$results
}

View File

@ -0,0 +1,386 @@
#!/usr/bin/env nu
# reflection/modules/config.nu — configuration profile management.
#
# Profiles live in reflection/configs/<profile>.ncl (mutable, git-versioned).
# Sealed history in reflection/configs/history/<profile>/cfg-NNN.ncl (append-only).
# Manifest at reflection/configs/manifest.ncl tracks active seal per profile.
#
# Seal = sha256(nickel export <profile.ncl>) written at apply time.
# Rollback = restore values from a history entry + write new seal.
# Verify = sha256 of current file == seal.hash of active history entry.
use env.nu *
use store.nu [daemon-export, daemon-export-safe]
export def "config show" [
profile: string,
--fmt: string = "",
] {
let f = if ($fmt | is-not-empty) { $fmt } else { "table" }
let file = (config-file $profile)
let data = (daemon-export $file)
let active = (config-active-seal $profile)
print ""
print $"Profile: ($profile | str upcase)"
print $"Active seal: ($active | default '(none)')"
print ""
match $f {
"json" => { print ($data | to json) },
_ => { print ($data | table) },
}
print ""
}
export def "config history" [
profile: string,
--fmt: string = "",
] {
let entries = (config-history-entries $profile)
if ($entries | is-empty) {
print $"No history for ($profile)"
return
}
let rows = $entries | each { |e|
{
id: $e.id,
applied_at: $e.seal.applied_at,
applied_by: $e.seal.applied_by,
hash: ($e.seal.hash | str substring 0..8),
adr: ($e.seal.related_adr? | default ""),
pr: ($e.seal.related_pr? | default ""),
bug: ($e.seal.related_bug? | default ""),
note: ($e.seal.note? | default ""),
}
}
match ($fmt | default "table") {
"json" => { print ($rows | to json) },
"md" => { print ($rows | to md) },
_ => { print ($rows | table) },
}
}
export def "config diff" [
profile: string,
from_id: string,
to_id: string,
] {
let root = (config-root)
let hist = $"($root)/reflection/configs/history/($profile | str downcase)"
let from_f = $"($hist)/($from_id).ncl"
let to_f = $"($hist)/($to_id).ncl"
for f in [$from_f, $to_f] {
if not ($f | path exists) { error make { msg: $"Config state ($f) not found" } }
}
let from_data = (daemon-export $from_f)
let to_data = (daemon-export $to_f)
# values_snapshot is stored as a JSON string inside the Nickel file
let from_vals = $from_data | get values_snapshot | from json
let to_vals = $to_data | get values_snapshot | from json
let from_json = $from_vals | to json --indent 2
let to_json = $to_vals | to json --indent 2
let tmp_from = (mktemp --suffix ".json")
let tmp_to = (mktemp --suffix ".json")
$from_json | save --force $tmp_from
$to_json | save --force $tmp_to
print $"diff ($from_id) → ($to_id)"
do { ^diff $tmp_from $tmp_to } | complete | get stdout | print
rm $tmp_from $tmp_to
}
export def "config verify" [profile: string] {
let file = (config-file $profile)
let active = (config-active-seal $profile)
if ($active | is-empty) {
print $" ($profile): no active seal — run 'ontoref config apply ($profile)' first"
return
}
let root = (config-root)
let hist_f = $"($root)/reflection/configs/history/($profile | str downcase)/($active).ncl"
if not ($hist_f | path exists) {
print $" ($profile): seal ($active) not found in history"
return
}
let hist_data = (daemon-export-safe $hist_f)
let stored_hash = if $hist_data != null { $hist_data | get seal.hash } else { "" }
let current_hash = (config-hash $file)
if $stored_hash == $current_hash {
print $" ($profile): ✓ verified ($current_hash | str substring 0..16)..."
} else {
print $" ($profile): ✗ DRIFT DETECTED"
print $" stored: ($stored_hash | str substring 0..16)..."
print $" current: ($current_hash | str substring 0..16)..."
print $" File was modified after last seal. Run 'ontoref config apply ($profile)' to reseal."
}
}
export def "config audit" [] {
let root = (config-root)
let manifest = (config-manifest)
print ""
print "CONFIG AUDIT"
print "────────────────────────────────────────"
for profile in ($manifest.profiles? | default []) {
config verify ($profile | into string | str downcase)
}
print ""
}
export def "config apply" [
profile: string,
--adr: string = "",
--pr: string = "",
--bug: string = "",
--note: string = "",
] {
let root = (config-root)
let file = (config-file $profile)
let actor = ($env.ONTOREF_ACTOR? | default "developer") | str replace --all " " "_"
let today = (date now | format date "%Y-%m-%dT%H:%M:%S")
let ts = (date now | format date "%Y%m%dT%H%M%S")
# Canonical enum label: "ci" → "CI", "development" → "Development"
let profile_key = (manifest-canonical-key $root $profile)
# Export + hash
let export_data = (daemon-export $file)
let hash = (config-hash $file)
# Collision-free ID: timestamp + actor — no sequential counter, no TOCTOU race
let hist_dir = $"($root)/reflection/configs/history/($profile | str downcase)"
^mkdir -p $hist_dir
let cfg_id = $"cfg-($ts)-($actor)"
let hist_f = $"($hist_dir)/($cfg_id).ncl"
let schema_rel = "../../../../reflection/schemas/config.ncl"
let prev_seal = (config-active-seal $profile | default "")
# values_snapshot stored as Nickel String (inline JSON is not valid Nickel —
# `:` is a contract separator, not a key-value delimiter).
let vals_json_raw = ($export_data | get values | to json)
let snap_hash = (config-string-hash $vals_json_raw)
# Escape for embedding inside a Nickel string literal.
# Single-quoted Nu strings are raw: '\"' = two chars (\, "), not one.
# Order matters: escape backslashes first so the new `\` from `\"` isn't re-escaped.
let vals_json_esc = (
$vals_json_raw
| str replace --all '\' '\\' # literal \ → \\ (single backslash → escaped backslash)
| str replace --all '"' '\"' # literal " → \" (quote → escaped quote)
)
let content = $"let s = import \"($schema_rel)\" in
{
id = \"($cfg_id)\",
profile = '($profile_key),
seal = {
hash = \"($hash)\",
snapshot_hash = \"($snap_hash)\",
applied_at = \"($today)\",
applied_by = \"($actor)\",
note = \"($note)\",
related_adr = \"($adr)\",
related_pr = \"($pr)\",
related_bug = \"($bug)\",
},
values_snapshot = \"($vals_json_esc)\",
supersedes = \"($prev_seal)\",
} | s.ConfigState
"
$content | save --force $hist_f
config-set-active $profile $cfg_id
print $" applied: ($profile) → ($cfg_id) hash=($hash | str substring 0..16)..."
if ($adr | str length) > 0 { print $" adr: ($adr)" }
if ($pr | str length) > 0 { print $" pr: ($pr)" }
if ($bug | str length) > 0 { print $" bug: ($bug)" }
}
export def "config rollback" [
profile: string,
to_id: string,
--adr: string = "",
--note: string = "",
] {
let root = (config-root)
let hist_dir = $"($root)/reflection/configs/history/($profile | str downcase)"
let src_f = $"($hist_dir)/($to_id).ncl"
if not ($src_f | path exists) { error make { msg: $"Config state ($to_id) not found" } }
let entry = (daemon-export $src_f)
let snapshot_json = ($entry | get values_snapshot) # raw JSON string
let stored_snap_h = ($entry | get seal.snapshot_hash) # sha256 of that string at apply time
let actual_snap_h = (config-string-hash $snapshot_json)
if $stored_snap_h != $actual_snap_h {
error make { msg: $"Integrity check failed for ($to_id): snapshot_hash mismatch. Rollback aborted." }
}
let snapshot = ($snapshot_json | from json)
let file = (config-file $profile)
let profile_key = (manifest-canonical-key $root $profile)
print $" rollback: ($profile) → ($to_id) [snapshot integrity verified]"
profile-file-write $file $profile_key $snapshot
print $" restored: ($file)"
config apply $profile --adr $adr --note $"rollback to ($to_id): ($note)"
}
# ── Internal ────────────────────────────────────────────────────────────────────
def config-root []: nothing -> string {
$env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT
}
def config-file [profile: string]: nothing -> string {
$"(config-root)/reflection/configs/($profile | str downcase).ncl"
}
def config-manifest []: nothing -> record {
let f = $"(config-root)/reflection/configs/manifest.ncl"
if not ($f | path exists) { return {} }
daemon-export-safe $f | default {}
}
def config-active-seal [profile: string]: nothing -> string {
let manifest = (config-manifest)
let key = (manifest-canonical-key (config-root) $profile)
$manifest.active? | default {} | get --optional $key | default ""
}
def config-set-active [profile: string, cfg_id: string] {
let root = (config-root)
let manifest_f = $"($root)/reflection/configs/manifest.ncl"
# Look up the canonical key from the manifest export — avoids str-capitalize mismatches
# (e.g. "ci" → "CI" not "Ci", "development" → "Development").
let key = (manifest-canonical-key $root $profile)
if ($key | is-empty) {
print $" warning: profile '($profile)' not found in manifest.ncl active map — add it manually"
return
}
let script = $"s/($key)[[:space:]]*=[[:space:]]*\"[^\"]*\"/($key) = \"($cfg_id)\"/"
let r1 = do { ^sed -i '' $script $manifest_f } | complete
if $r1.exit_code != 0 {
let r2 = do { ^sed -i $script $manifest_f } | complete
if $r2.exit_code != 0 {
print $" warning: could not patch manifest.ncl active.($key) — update manually to ($cfg_id)"
}
}
}
# Resolve the canonical key name as it appears in manifest.ncl active map.
# Matches case-insensitively so "ci" → "CI", "development" → "Development".
def manifest-canonical-key [root: string, profile: string]: nothing -> string {
let manifest_f = $"($root)/reflection/configs/manifest.ncl"
let data = (daemon-export-safe $manifest_f)
if $data == null { return ($profile | str capitalize) }
let keys = ($data | get active | columns)
$keys | where { |k| ($k | str downcase) == ($profile | str downcase) } | first | default ($profile | str capitalize)
}
def config-hash [file: string]: nothing -> string {
let r = do { ^nickel export $file } | complete
if $r.exit_code != 0 { return "" }
let tmp = (mktemp --suffix ".json")
$r.stdout | save --force $tmp
let hash = do {
let h = (do { ^sha256sum $tmp } | complete)
if $h.exit_code == 0 { $h.stdout | split words | first } else {
# macOS shasum fallback
let h2 = do { ^shasum -a 256 $tmp } | complete
if $h2.exit_code == 0 { $h2.stdout | split words | first } else { "" }
}
}
rm $tmp
$hash
}
# Hash an arbitrary string (not a file). Used for snapshot_hash in seals.
def config-string-hash [s: string]: nothing -> string {
let tmp = (mktemp --suffix ".json")
$s | save --force $tmp
let hash = do {
let h = (do { ^sha256sum $tmp } | complete)
if $h.exit_code == 0 { $h.stdout | split words | first } else {
let h2 = do { ^shasum -a 256 $tmp } | complete
if $h2.exit_code == 0 { $h2.stdout | split words | first } else { "" }
}
}
rm $tmp
$hash
}
# Recursively convert a Nu value to a Nickel literal string.
# depth controls indentation (2-space units).
def nickel-indent [n: int]: nothing -> string {
0..<$n | reduce --fold "" { |_, acc| $acc + " " }
}
def value-to-nickel [v: any, depth: int = 0]: nothing -> string {
let pad = (nickel-indent $depth)
let pad2 = (nickel-indent ($depth + 1))
let type = ($v | describe | str replace --regex '<.*>' '')
if $type == "string" {
let s = ($v | into string)
let esc = ($s | str replace --all '\\' '\\\\' | str replace --all '"' '\\"')
'"' + $esc + '"'
} else if $type == "int" {
$v | into string
} else if $type == "float" {
$v | into string
} else if $type == "bool" {
$v | into string
} else if $type == "list" {
if ($v | is-empty) { "[]" } else {
let items = ($v | each { |item| $"($pad2)(value-to-nickel $item ($depth + 1))" } | str join ",\n")
$"[\n($items),\n($pad)]"
}
} else if $type == "record" {
let fields = ($v | transpose k val | each { |f|
$"($pad2)($f.k) = (value-to-nickel $f.val ($depth + 1)),"
} | str join "\n")
$"{\n($fields)\n($pad)}"
} else {
$v | to json
}
}
# Reconstruct a profile .ncl file from the canonical profile key and a values record.
# Preserves the file's import line; replaces the values block wholesale.
def profile-file-write [file: string, profile_key: string, values: record] {
let vals_nickel = (value-to-nickel $values 2)
let content = $"let s = import \"../schemas/config.ncl\" in\n\n\{\n profile = '($profile_key),\n\n values = ($vals_nickel),\n}\n"
$content | save --force $file
}
def config-history-entries [profile: string] {
let root = (config-root)
let hist_dir = $"($root)/reflection/configs/history/($profile | str downcase)"
if not ($hist_dir | path exists) { return [] }
ls $hist_dir
| where name =~ 'cfg-.*\.ncl$'
| sort-by name
| each { |f|
let d = (daemon-export-safe $f.name)
if $d != null {
{ id: $d.id, seal: $d.seal, supersedes: ($d.supersedes? | default "") }
} else { null }
}
| compact
}

File diff suppressed because it is too large Load Diff

32
reflection/modules/env.nu Normal file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env nu
# reflection/modules/env.nu — environment bootstrap for ontoref.
# Sets: ONTOREF_ROOT, NICKEL_IMPORT_PATH, ONTOREF_ACTOR
export-env {
let root = (
$env.CURRENT_FILE
| path dirname # reflection/modules
| path dirname # reflection
| path dirname # <root>
)
$env.ONTOREF_ROOT = $root
if ($env.NICKEL_IMPORT_PATH? | is-empty) {
let config_file = $"($root)/.ontoref/config.ncl"
# Guard: config must exist and nickel must be in PATH before reading
let paths = if ($config_file | path exists) and (which nickel | is-not-empty) {
do { ^nickel export $config_file } | complete
| if $in.exit_code == 0 {
$in.stdout | from json | get nickel_import_paths
| each { |p| $"($root)/($p)" }
} else { [$root] }
} else { [$root] }
$env.NICKEL_IMPORT_PATH = ($paths | str join ":")
}
if ($env.ONTOREF_ACTOR? | is-empty) {
$env.ONTOREF_ACTOR = "developer"
}
}

202
reflection/modules/forms.nu Normal file
View File

@ -0,0 +1,202 @@
#!/usr/bin/env nu
# reflection/modules/forms.nu — form listing and execution.
#
# Error handling policy:
# forms list — daemon-export-safe; graceful skip on broken form file
# form run — daemon-export (fail loud); agent path renders NCL template via tera
# render-* — tera-render direct; fail loud on template error
use env.nu *
use store.nu [daemon-export, daemon-export-safe]
export def "forms list" [] {
form-files | each { |f|
let data = (daemon-export-safe $f)
if $data != null {
{
name: ($f | path basename | str replace ".ncl" ""),
description: ($data.description? | default ""),
}
} else { null }
} | compact
}
export def "form run" [
name: string,
--backend: string = "cli", # Backend: cli (default) | tui | web
] {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let forms_dir = $"($env.ONTOREF_ROOT)/reflection/forms"
let form_file = $"($forms_dir)/($name).ncl"
# Guard: form must exist
if not ($form_file | path exists) {
let available = (form-files | each { |f| $f | path basename | str replace ".ncl" "" })
print $"Form '($name)' not found."
print $"Available: ($available | str join ', ')"
exit 1
}
if $actor == "agent" {
agent-render $name $form_file
return
}
# Guard: typedialog must be present for interactive path
if (which typedialog | is-empty) {
print ""
print "typedialog is required for interactive forms."
print "Install: cargo install typedialog"
print ""
print "Agent path (no typedialog needed):"
agent-render-hint $name $form_file
exit 1
}
run-interactive-form $name $form_file $backend
}
# Render an ADR from a piped record via tera-render.
# { id: "adr-005", title: "...", status: "Proposed" } | form render-adr | save adrs/adr-005-title.ncl
export def "form render-adr" [
--output: path,
] {
let tmpl = $"($env.ONTOREF_ROOT)/reflection/templates/adr.ncl.j2"
let rendered = $in | tera-render $tmpl
if ($output | is-not-empty) {
$rendered | save --force $output
print $"Written: ($output)"
} else {
$rendered
}
}
# Render a new-project script from a piped record via tera-render.
# { project_name: "myapp", project_dir: "/tmp/myapp" } | form render-project --output ~/create.nu
export def "form render-project" [
--output: path,
] {
let tmpl = $"($env.ONTOREF_ROOT)/reflection/templates/create_project.nu.j2"
let rendered = $in | tera-render $tmpl
if ($output | is-not-empty) {
$rendered | save --force $output
print $"Written: ($output)"
} else {
$rendered
}
}
export def "forms help" [] {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
print ""
print "Form commands:"
print " forms list list available forms"
print " form run <name> run a form (interactive or agent)"
print " form render-adr [--output <path>] render ADR from piped record"
print " form render-project [--output] render new-project script from piped record"
print ""
for f in (forms list) {
print $" ($f.name)"
print $" ($f.description)"
}
if $actor == "agent" {
print ""
print "Agent render pipeline:"
print " nickel-export adrs/_template.ncl | form render-adr --output adrs/adr-005-title.ncl"
print " { project_name: 'myapp', ... } | form render-project --output ~/create.nu"
print ""
print "Inspect form fields:"
print " nickel-export reflection/forms/<name>.ncl | get elements"
print " | where type != 'section_header' | select name prompt required nickel_path"
}
print ""
}
# ── Internal ───────────────────────────────────────────────────────────────────
def form-files [] {
glob $"($env.ONTOREF_ROOT)/reflection/forms/*.ncl"
}
def next-adr-id [] {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let nums = (
glob $"($root)/adrs/adr-*.ncl"
| each { |f| $f | path basename }
| parse "adr-{n}-{rest}.ncl"
| get n
| each { |n| $n | into int }
| sort
)
let next = if ($nums | is-empty) { 1 } else { ($nums | last) + 1 }
$"adr-($next | fill --alignment right --width 3 --character '0')"
}
def agent-render [name: string, form_file: string] {
if $name == "new_adr" {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let template = $"($env.ONTOREF_ROOT)/adrs/_template.ncl"
let tmpl = $"($env.ONTOREF_ROOT)/reflection/templates/adr.ncl.j2"
let next_id = (next-adr-id)
let out = $"($root)/adrs/($next_id)-draft.ncl"
print $"Rendering ($next_id) from template — output: ($out)"
daemon-export $template | to json | tera-render $tmpl | save --force $out
print $"Run: nickel-validate ($out)"
} else {
agent-render-hint $name $form_file
}
}
def agent-render-hint [name: string, form_file: string] {
print ""
print $"Fields for '($name)':"
print $" nickel-export ($form_file) | get elements"
print " | where type != \"section_header\" | select name prompt required nickel_path"
print ""
}
def td-roundtrip [backend: string, input: string, form: string, output: string, ncl_template: string] {
match $backend {
"web" => { ^typedialog web nickel-roundtrip $input $form --output $output --ncl-template $ncl_template --open },
"tui" => { ^typedialog tui nickel-roundtrip $input $form --output $output --ncl-template $ncl_template },
_ => { ^typedialog nickel-roundtrip $input $form --output $output --ncl-template $ncl_template },
}
}
def td-form [backend: string, form: string, ...extra: string] {
match $backend {
"web" => { ^typedialog web form $form ...$extra },
"tui" => { ^typedialog tui $form ...$extra },
_ => { ^typedialog form $form ...$extra },
}
}
def run-interactive-form [name: string, form_file: string, backend: string] {
if $name == "new_adr" {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let template = $"($env.ONTOREF_ROOT)/adrs/_template.ncl"
let tmpl = $"($env.ONTOREF_ROOT)/reflection/templates/adr.ncl.j2"
let next_id = (next-adr-id)
let out = $"($root)/adrs/($next_id)-draft.ncl"
let tmp = $"($env.HOME)/.ontoref-adr-tmp.ncl"
open $template | str replace '"adr-000"' $'"($next_id)"' | save --force $tmp
print $"Creating ($next_id) — output: ($out)"
td-roundtrip $backend $tmp $form_file $out $tmpl
rm --force $tmp
print $"Run: nickel typecheck ($out)"
} else if $name == "new_project" {
let tmpl = $"($env.ONTOREF_ROOT)/reflection/templates/create_project.nu.j2"
let out = $"($env.HOME)/ontoref-create-project.nu"
td-form $backend $form_file "--ncl-template" $tmpl "--output" $out
print $"Script generated: ($out)"
print $"Review then: nu ($out)"
} else if $name == "supersede_adr" {
let tmpl = $"($env.ONTOREF_ROOT)/reflection/templates/supersede_adr.nu.j2"
let out = $"($env.HOME)/ontoref-supersede.nu"
td-form $backend $form_file "--ncl-template" $tmpl "--output" $out
print $"Script generated: ($out)"
print $"Review then: nu ($out)"
} else {
td-form $backend $form_file
}
}

View File

@ -0,0 +1,535 @@
#!/usr/bin/env nu
# reflection/modules/generator.nu — project output generators.
# Composes full documentation from ontology, ADRs, modes, crates, and scenarios.
# Output formats: md (human), json (agent/MCP), mdbook (publishable HTML).
#
# This is the implementation behind the generate-docs and generate-mdbook modes.
# Modes declare steps; this module does the actual work.
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 }
}
use ../modules/store.nu [daemon-export-safe]
def nickel-import-path [root: string]: nothing -> string {
let entries = [
$"($root)/.ontology"
$"($root)/adrs"
$"($root)/.ontoref/ontology/schemas"
$"($root)/.ontoref/adrs"
$"($root)/.onref"
$root
$"($env.ONTOREF_ROOT)/ontology"
$"($env.ONTOREF_ROOT)/ontology/schemas"
$"($env.ONTOREF_ROOT)/adrs"
$env.ONTOREF_ROOT
]
let valid = ($entries | where { |p| $p | path exists } | uniq)
let existing = ($env.NICKEL_IMPORT_PATH? | default "")
if ($existing | is-not-empty) {
($valid | append $existing) | str join ":"
} else {
$valid | str join ":"
}
}
def ncl-export-safe [root: string, file: string]: nothing -> record {
let ip = (nickel-import-path $root)
daemon-export-safe $file --import-path $ip | default {}
}
# ── Data extraction ──────────────────────────────────────────────────────────
def extract-identity [root: string]: nothing -> record {
let cargo = $"($root)/Cargo.toml"
mut name = ($root | path basename)
mut version = ""
mut description = ""
mut crates = []
if ($cargo | path exists) {
let cargo_data = (open $cargo)
if ($cargo_data | get -o package.name | is-not-empty) {
$name = ($cargo_data | get package.name)
$version = ($cargo_data | get -o package.version | default "")
$description = ($cargo_data | get -o package.description | default "")
}
# Workspace members
let members = ($cargo_data | get -o workspace.members | default [])
if ($members | is-not-empty) {
$crates = ($members | each { |m|
let member_cargo = $"($root)/($m)/Cargo.toml"
# Expand globs: crates/* → actual directories
if ($m | str contains "*") {
glob $"($root)/($m)/Cargo.toml" | each { |f|
let crate_dir = ($f | path dirname)
let crate_data = (open $f)
{
name: ($crate_data | get -o package.name | default ($crate_dir | path basename)),
path: ($crate_dir | str replace $"($root)/" ""),
description: ($crate_data | get -o package.description | default ""),
version: ($crate_data | get -o package.version | default ""),
}
}
} else if ($member_cargo | path exists) {
let crate_data = (open $member_cargo)
[{
name: ($crate_data | get -o package.name | default ($m | path basename)),
path: $m,
description: ($crate_data | get -o package.description | default ""),
version: ($crate_data | get -o package.version | default ""),
}]
} else { [] }
} | flatten)
}
}
{ name: $name, version: $version, description: $description, crates: $crates, root: $root }
}
def extract-ontology [root: string]: nothing -> record {
let core_file = $"($root)/.ontology/core.ncl"
if not ($core_file | path exists) { return { nodes: [], edges: [] } }
let core = (ncl-export-safe $root $core_file)
if ($core | is-empty) { return { nodes: [], edges: [] } }
{
nodes: ($core.nodes? | default []),
edges: ($core.edges? | default []),
}
}
def extract-state [root: string]: nothing -> list<record> {
let state_file = $"($root)/.ontology/state.ncl"
if not ($state_file | path exists) { return [] }
let state = (ncl-export-safe $root $state_file)
if ($state | is-empty) { return [] }
$state.dimensions? | default []
}
def extract-gates [root: string]: nothing -> list<record> {
let gate_file = $"($root)/.ontology/gate.ncl"
if not ($gate_file | path exists) { return [] }
let gate = (ncl-export-safe $root $gate_file)
if ($gate | is-empty) { return [] }
$gate.gates? | default [] | where { |g| ($g.active? | default false) == true }
}
def extract-adrs [root: string]: nothing -> list<record> {
let files = (glob $"($root)/adrs/adr-*.ncl")
$files | each { |f|
let data = (ncl-export-safe $root $f)
if ($data | is-not-empty) {
{
id: ($data.id? | default ""),
title: ($data.title? | default ""),
status: ($data.status? | default ""),
date: ($data.date? | default ""),
decision: ($data.decision? | default ""),
constraints: ($data.constraints? | default []),
}
} else { null }
} | compact
}
def extract-modes [root: string]: nothing -> list<record> {
let ontoref_modes = (glob $"($env.ONTOREF_ROOT)/reflection/modes/*.ncl")
let project_modes = if $root != $env.ONTOREF_ROOT {
glob $"($root)/reflection/modes/*.ncl"
} else { [] }
let all_files = ($ontoref_modes | append $project_modes | uniq)
$all_files | each { |f|
let data = (ncl-export-safe $root $f)
if ($data | is-not-empty) and ($data.id? | is-not-empty) {
{
id: ($data.id? | default ""),
trigger: ($data.trigger? | default ""),
steps: ($data.steps? | default [] | length),
source: (if ($f | str starts-with $root) { $f | str replace $"($root)/" "" } else { $f | str replace $"($env.ONTOREF_ROOT)/" "onref/" }),
}
} else { null }
} | compact
}
def extract-scenarios [root: string]: nothing -> list<record> {
let scenarios_dir = $"($root)/reflection/scenarios"
if not ($scenarios_dir | path exists) { return [] }
let dirs = (ls $scenarios_dir | where type == dir | get name)
$dirs | each { |d|
let scenario_ncl = $"($d)/scenario.ncl"
let meta = if ($scenario_ncl | path exists) {
ncl-export-safe $root $scenario_ncl
} else { {} }
let files = (ls $d | where type == file | get name | each { |f| $f | path basename })
{
category: ($d | path basename),
path: ($d | str replace $"($root)/" ""),
files: $files,
actor: ($meta.actor? | default ""),
purpose: ($meta.purpose? | default ""),
}
}
}
# ── Compose full document ────────────────────────────────────────────────────
def compose-doc-data [root: string]: nothing -> record {
let identity = (extract-identity $root)
let ontology = (extract-ontology $root)
let state = (extract-state $root)
let gates = (extract-gates $root)
let adrs = (extract-adrs $root)
let modes = (extract-modes $root)
let scenarios = (extract-scenarios $root)
# Classify ontology nodes by level.
let nodes = ($ontology.nodes? | default [])
let axioms = ($nodes | where { |n| ($n.invariant? | default false) == true })
let tensions = ($nodes | where { |n| ($n.level? | default "") == "Tension" })
let practices = ($nodes | where { |n| let l = ($n.level? | default ""); $l == "Practice" or $l == "Spiral" })
{
identity: $identity,
architecture: {
axioms: ($axioms | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default ""), artifact_paths: ($n.artifact_paths? | default []) } }),
tensions: ($tensions | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default "") } }),
practices: ($practices | each { |n| { id: $n.id, name: ($n.name? | default ""), description: ($n.description? | default ""), artifact_paths: ($n.artifact_paths? | default []) } }),
edges: ($ontology.edges? | default []),
},
state: ($state | each { |d| { id: ($d.id? | default ""), name: ($d.name? | default ""), current: ($d.current_state? | default ""), desired: ($d.desired_state? | default "") } }),
gates: ($gates | each { |g| { id: ($g.id? | default ""), protects: ($g.protects? | default []), condition: ($g.opening_condition? | default "") } }),
decisions: ($adrs | where { |a| $a.status == "Accepted" }),
decisions_all: $adrs,
modes: $modes,
scenarios: $scenarios,
}
}
# ── Format: Markdown ─────────────────────────────────────────────────────────
def render-md [data: record]: nothing -> string {
let id = $data.identity
mut lines = [$"# ($id.name)"]
if ($id.version | is-not-empty) { $lines = ($lines | append $"**Version**: ($id.version)") }
if ($id.description | is-not-empty) {
$lines = ($lines | append "")
$lines = ($lines | append $id.description)
}
$lines = ($lines | append "")
# Crates
if ($id.crates | is-not-empty) {
$lines = ($lines | append "## Crates")
$lines = ($lines | append "")
$lines = ($lines | append "| Crate | Description |")
$lines = ($lines | append "|-------|-------------|")
for c in $id.crates {
$lines = ($lines | append $"| `($c.name)` | ($c.description) |")
}
$lines = ($lines | append "")
}
# Architecture — Axioms
if ($data.architecture.axioms | is-not-empty) {
$lines = ($lines | append "## Invariants")
$lines = ($lines | append "")
for a in $data.architecture.axioms {
$lines = ($lines | append $"### ($a.name)")
$lines = ($lines | append "")
$lines = ($lines | append $a.description)
if ($a.artifact_paths | is-not-empty) {
let paths = ($a.artifact_paths | each { |p| $"`($p)`" } | str join ", ")
$lines = ($lines | append $"Artifacts: ($paths)")
}
$lines = ($lines | append "")
}
}
# Architecture — Tensions
if ($data.architecture.tensions | is-not-empty) {
$lines = ($lines | append "## Tensions")
$lines = ($lines | append "")
for t in $data.architecture.tensions {
let poles = if ($t.poles | is-not-empty) {
let pole_strs = ($t.poles | each { |p| $"($p.pole? | default '')" })
let joined = ($pole_strs | str join " ↔ ")
$" (char lparen)($joined)(char rparen)"
} else { "" }
$lines = ($lines | append $"- **($t.name)**($poles): ($t.description)")
}
$lines = ($lines | append "")
}
# Architecture — Systems/Practices
if ($data.architecture.practices | is-not-empty) {
$lines = ($lines | append "## Systems")
$lines = ($lines | append "")
for p in $data.architecture.practices {
$lines = ($lines | append $"### ($p.name)")
$lines = ($lines | append "")
$lines = ($lines | append $p.description)
if ($p.artifact_paths | is-not-empty) {
$lines = ($lines | append "")
for ap in $p.artifact_paths { $lines = ($lines | append $"- `($ap)`") }
}
$lines = ($lines | append "")
}
}
# State dimensions
if ($data.state | is-not-empty) {
$lines = ($lines | append "## State")
$lines = ($lines | append "")
$lines = ($lines | append "| Dimension | Current | Desired |")
$lines = ($lines | append "|-----------|---------|---------|")
for d in $data.state {
let marker = if $d.current == $d.desired { " ✓" } else { "" }
$lines = ($lines | append $"| ($d.name) | `($d.current)` | `($d.desired)`($marker) |")
}
$lines = ($lines | append "")
}
# Gates
if ($data.gates | is-not-empty) {
$lines = ($lines | append "## Active Gates")
$lines = ($lines | append "")
for g in $data.gates {
$lines = ($lines | append $"- **($g.id)**: protects `($g.protects)` — ($g.condition)")
}
$lines = ($lines | append "")
}
# Decisions
if ($data.decisions | is-not-empty) {
$lines = ($lines | append "## Decisions (ADRs)")
$lines = ($lines | append "")
for a in $data.decisions {
$lines = ($lines | append $"### ($a.id): ($a.title)")
$lines = ($lines | append "")
$lines = ($lines | append $a.decision)
if ($a.constraints | is-not-empty) {
let hard = ($a.constraints | where { |c| ($c.severity? | default "") == "Hard" })
if ($hard | is-not-empty) {
$lines = ($lines | append "")
$lines = ($lines | append "**Hard constraints:**")
$lines = ($lines | append "")
for c in $hard {
$lines = ($lines | append $"- ($c.description? | default $c.scope)")
}
}
}
$lines = ($lines | append "")
}
}
# Modes
if ($data.modes | is-not-empty) {
$lines = ($lines | append "## Operational Modes")
$lines = ($lines | append "")
for m in $data.modes {
let step_count = $"(char lparen)($m.steps) steps(char rparen)"
$lines = ($lines | append $"- **($m.id)** ($step_count): ($m.trigger)")
}
$lines = ($lines | append "")
}
# Scenarios
if ($data.scenarios | is-not-empty) {
$lines = ($lines | append "## Scenarios")
$lines = ($lines | append "")
for s in $data.scenarios {
let purpose_str = if ($s.purpose | is-not-empty) { $" [($s.purpose)]" } else { "" }
let file_count = $"(char lparen)($s.files | length) files(char rparen)"
$lines = ($lines | append $"- **($s.category)**($purpose_str): `($s.path)/` ($file_count)")
}
$lines = ($lines | append "")
}
$lines | str join "\n"
}
# ── Format: mdBook ───────────────────────────────────────────────────────────
def render-mdbook [data: record, root: string] {
let docs_src = $"($root)/docs/src"
mkdir $docs_src
mkdir $"($docs_src)/architecture"
mkdir $"($docs_src)/decisions"
mkdir $"($docs_src)/modes"
# README / intro
let intro = ([
$"# ($data.identity.name)"
""
($data.identity.description)
""
$"Generated from project ontology and reflection data."
] | str join "\n")
$intro | save -f $"($docs_src)/README.md"
# Architecture page
let arch_data = {
identity: $data.identity,
architecture: $data.architecture,
state: $data.state,
gates: $data.gates,
decisions: [],
decisions_all: [],
modes: [],
scenarios: [],
}
let arch_md = (render-md $arch_data)
$arch_md | save -f $"($docs_src)/architecture/overview.md"
# Individual ADR pages
for adr in $data.decisions_all {
let adr_data = {
identity: { name: "", version: "", description: "", crates: [], root: "" },
architecture: { axioms: [], tensions: [], practices: [], edges: [] },
state: [],
gates: [],
decisions: (if $adr.status == "Accepted" { [$adr] } else { [] }),
decisions_all: [$adr],
modes: [],
scenarios: [],
}
let status_badge = match $adr.status {
"Accepted" => "✅ Accepted",
"Proposed" => "📝 Proposed",
"Superseded" => "🔄 Superseded",
_ => $adr.status,
}
let content = ([
$"# ($adr.id): ($adr.title)"
""
$"**Status**: ($status_badge) **Date**: ($adr.date)"
""
$adr.decision
] | str join "\n")
$content | save -f $"($docs_src)/decisions/($adr.id).md"
}
# ADR index
let adr_index_lines = (["# Decisions (ADRs)" ""] | append (
$data.decisions_all | each { |a|
let link = $"(char lparen)./($a.id).md(char rparen)"
$"- [($a.id): ($a.title)]($link) — ($a.status)"
}
))
($adr_index_lines | str join "\n") | save -f $"($docs_src)/decisions/README.md"
# Modes page
let modes_lines = (["# Operational Modes" ""] | append (
$data.modes | each { |m|
let step_count = $"(char lparen)($m.steps) steps(char rparen)"
$"- **($m.id)** ($step_count): ($m.trigger)"
}
))
($modes_lines | str join "\n") | save -f $"($docs_src)/modes/README.md"
# SUMMARY.md
mut summary = [
"# Summary"
""
"[Introduction](README.md)"
""
"# Architecture"
""
"- [Overview](architecture/overview.md)"
""
"# Decisions"
""
]
for adr in $data.decisions_all {
$summary = ($summary | append $"- [($adr.id)](decisions/($adr.id).md)")
}
$summary = ($summary | append ["" "# Operations" "" "- [Modes](modes/README.md)"])
($summary | str join "\n") | save -f $"($docs_src)/SUMMARY.md"
# book.toml
let book_toml = $"[book]
authors = [\"Generated by ontoref\"]
language = \"en\"
multilingual = false
src = \"src\"
title = \"($data.identity.name) Documentation\"
[output.html]
default-theme = \"navy\"
preferred-dark-theme = \"navy\"
"
let book_file = $"($root)/docs/book.toml"
if not ($book_file | path exists) {
$book_toml | save -f $book_file
}
print $" (ansi green)Generated:(ansi reset) ($docs_src)/SUMMARY.md"
print $" (ansi green)Generated:(ansi reset) ($docs_src)/README.md"
print $" (ansi green)Generated:(ansi reset) ($docs_src)/architecture/overview.md"
let adr_count = ($data.decisions_all | length)
print $" (ansi green)Generated:(ansi reset) ($adr_count) ADR pages in ($docs_src)/decisions/"
print $" (ansi green)Generated:(ansi reset) ($docs_src)/modes/README.md"
# Build if mdbook is available
let has_mdbook = (do { ^which mdbook } | complete | get exit_code) == 0
if $has_mdbook {
print ""
print $" (ansi cyan)Building mdBook...(ansi reset)"
let result = (do { ^mdbook build $"($root)/docs/" } | complete)
if $result.exit_code == 0 {
print $" (ansi green)Book built:(ansi reset) ($root)/docs/book/"
} else {
print $" (ansi yellow)Build failed:(ansi reset) ($result.stderr | str trim)"
print $" (ansi dark_gray)Run manually: mdbook build docs/(ansi reset)"
}
} else {
print ""
print $" (ansi dark_gray)mdbook not found. Install: cargo install mdbook(ansi reset)"
print $" (ansi dark_gray)Then: mdbook build docs/(ansi reset)"
}
}
# ── Public API ───────────────────────────────────────────────────────────────
export def "docs generate" [
--fmt (-f): string = "", # Output format: md | json | yaml | mdbook (short: m j y)
]: nothing -> nothing {
let root = (project-root)
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let raw_fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "md" }
let f = match $raw_fmt {
"j" => "json",
"y" => "yaml",
"m" => "md",
_ => $raw_fmt,
}
let data = (compose-doc-data $root)
match $f {
"json" => { print ($data | to json) },
"yaml" => { print ($data | to yaml) },
"md" => { print (render-md $data) },
"mdbook" => { render-mdbook $data $root },
_ => {
print $" Unknown format: ($f). Available: md | json | yaml | mdbook"
},
}
}
export def "docs formats" []: nothing -> nothing {
print ""
print " Available documentation formats:"
print ""
print $" (ansi cyan)md(ansi reset) Markdown document to stdout (default for humans)"
print $" (ansi cyan)json(ansi reset) Structured JSON to stdout (default for agents)"
print $" (ansi cyan)yaml(ansi reset) YAML to stdout"
print $" (ansi cyan)mdbook(ansi reset) Generates docs/src/ + SUMMARY.md, builds if mdbook installed"
print ""
print $" (ansi dark_gray)Usage: strat docs generate --fmt <format>(ansi reset)"
print $" (ansi dark_gray)Short: strat docs generate -f j(ansi reset)"
print ""
}

View File

@ -0,0 +1,348 @@
use store.nu [daemon-export]
# Manifest operations — operational modes and publication services.
#
# Reads $ONTOREF_PROJECT_ROOT/.ontology/manifest.ncl to determine:
# - layers, operational_modes, consumption_modes, publication_services
#
# Commands:
# manifest load export and parse the project manifest
# manifest mode <id> switch to an operational mode (run pre/post activate)
# manifest mode list list available operational modes
# manifest publish <service-id> run a publication workflow
# manifest publish list list available publication services
# manifest layers show layers with committed status
# manifest consumers show consumption modes
def manifest-path []: nothing -> string {
let root = if ($env.ONTOREF_PROJECT_ROOT? | is-not-empty) {
$env.ONTOREF_PROJECT_ROOT
} else {
$env.PWD
}
$"($root)/.ontology/manifest.ncl"
}
# Export and parse the project manifest NCL
export def "manifest load" []: nothing -> record {
let path = (manifest-path)
if not ($path | path exists) {
error make { msg: $"No manifest at ($path) — create .ontology/manifest.ncl first" }
}
# Build import path: project .ontology/ + ontoref ontology/ + existing NICKEL_IMPORT_PATH
let project_ontology = ($path | path dirname)
let ontoref_ontology = if ($env.ONTOREF_ROOT? | is-not-empty) {
$"($env.ONTOREF_ROOT)/ontology"
} else {
""
}
let extra_paths = ([$project_ontology, $ontoref_ontology] | where { $in | is-not-empty })
let existing = ($env.NICKEL_IMPORT_PATH? | default "")
let import_path = if ($existing | is-not-empty) {
($extra_paths | append ($existing | split row ":") | uniq | str join ":")
} else {
($extra_paths | str join ":")
}
daemon-export $path --import-path $import_path
}
# Find an operational mode by id
def find-mode [manifest: record, id: string]: nothing -> record {
let modes = ($manifest.operational_modes? | default [])
let found = ($modes | where { ($in.id? | default "") == $id })
if ($found | is-empty) {
let available = ($modes | each { $in.id? | default "?" } | str join ", ")
error make { msg: $"Mode '($id)' not found. Available: ($available)" }
}
$found | first
}
# Find a publication service by id
def find-service [manifest: record, id: string]: nothing -> record {
let services = ($manifest.publication_services? | default [])
let found = ($services | where { ($in.id? | default "") == $id })
if ($found | is-empty) {
let available = ($services | each { $in.id? | default "?" } | str join ", ")
error make { msg: $"Service '($id)' not found. Available: ($available)" }
}
$found | first
}
# Run a list of shell commands sequentially, abort on failure
def run-commands [commands: list<string>, label: string]: nothing -> bool {
for cmd in $commands {
print $" [$label] ($cmd)"
let result = (do { nu -c $cmd } | complete)
if $result.exit_code != 0 {
print $" [$label] FAILED: ($cmd)"
if ($result.stderr | is-not-empty) {
print $" ($result.stderr | lines | first 5 | str join '\n ')"
}
return false
}
print $" [$label] OK"
}
true
}
# Switch to an operational mode — runs pre_activate, shows status, runs post_activate
export def "manifest mode" [
id: string # Mode id: dev, publish, investigate, ci
--dry-run (-n) # Show what would happen without executing
]: nothing -> record {
let m = (manifest load)
let mode = (find-mode $m $id)
let layers = ($m.layers? | default [])
let visible = ($mode.visible_layers? | default [])
let hidden = ($layers | where { not ($in.id in $visible) } | each { $in.id })
print ""
print $"── Mode: ($mode.id) ──"
if ($mode.description? | is-not-empty) {
print $" ($mode.description)"
}
print ""
print $" Audit level: ($mode.audit_level)"
print $" Visible layers: ($visible | str join ', ')"
if ($hidden | is-not-empty) {
print $" Hidden layers: ($hidden | str join ', ')"
}
let pre = ($mode.pre_activate? | default [])
let post = ($mode.post_activate? | default [])
if $dry_run {
if ($pre | is-not-empty) {
print ""
print " Would run pre_activate:"
for cmd in $pre { print $" ($cmd)" }
}
if ($post | is-not-empty) {
print ""
print " Would run post_activate:"
for cmd in $post { print $" ($cmd)" }
}
print ""
return { mode: $id, status: "dry-run", pre_ok: true, post_ok: true }
}
mut pre_ok = true
if ($pre | is-not-empty) {
print ""
print " pre_activate:"
$pre_ok = (run-commands $pre "pre")
if not $pre_ok {
print ""
print $" Mode activation ABORTED — pre_activate failed."
print $" Fix the issues and retry: ontoref mode ($id)"
return { mode: $id, status: "failed", pre_ok: false, post_ok: false }
}
}
mut post_ok = true
if ($post | is-not-empty) {
print ""
print " post_activate:"
$post_ok = (run-commands $post "post")
if not $post_ok {
print ""
print $" WARNING: post_activate failed. Mode is active but not fully verified."
}
}
let status = if $pre_ok and $post_ok { "active" } else { "partial" }
print ""
print $" Mode '($id)' → ($status)"
{ mode: $id, status: $status, pre_ok: $pre_ok, post_ok: $post_ok }
}
# List available operational modes
export def "manifest mode list" [
--fmt: string = "table" # Output format: table | json
]: nothing -> table {
let m = (manifest load)
let modes = ($m.operational_modes? | default [])
let default_mode = ($m.default_mode? | default "dev")
let rows = ($modes | each {|mode|
{
id: $mode.id,
description: ($mode.description? | default ""),
audit: ($mode.audit_level? | default "Standard"),
layers: ($mode.visible_layers? | default [] | length),
pre: ($mode.pre_activate? | default [] | length),
post: ($mode.post_activate? | default [] | length),
default: ($mode.id == $default_mode),
}
})
match $fmt {
"json" => { $rows | to json }
_ => { $rows }
}
}
# Run a publication workflow — pre_publish, confirmation, post_publish
export def "manifest publish" [
id: string # Service id: crates-io-public, gitea-private, etc.
--dry-run (-n) # Show what would happen without executing
--yes (-y) # Skip confirmation prompt
]: nothing -> record {
let m = (manifest load)
let svc = (find-service $m $id)
print ""
print $"── Publish: ($svc.id) ──"
print $" Artifact: ($svc.artifact)"
print $" Scope: ($svc.scope)"
if ($svc.registry_url? | is-not-empty) {
print $" Registry: ($svc.registry_url)"
}
print $" Auth: ($svc.auth_method)"
print $" Trigger: ($svc.trigger)"
if ($svc.condition? | is-not-empty) {
print $" Condition: ($svc.condition)"
}
let pre = ($svc.pre_publish? | default [])
let post = ($svc.post_publish? | default [])
if $dry_run {
if ($pre | is-not-empty) {
print ""
print " Would run pre_publish:"
for cmd in $pre { print $" ($cmd)" }
}
print ""
print " Would execute publish action"
if ($post | is-not-empty) {
print ""
print " Would run post_publish:"
for cmd in $post { print $" ($cmd)" }
}
print ""
return { service: $id, status: "dry-run" }
}
# Pre-publish checks
mut pre_ok = true
if ($pre | is-not-empty) {
print ""
print " pre_publish:"
$pre_ok = (run-commands $pre "pre")
if not $pre_ok {
print ""
print $" Publish ABORTED — pre_publish failed."
return { service: $id, status: "failed", stage: "pre_publish" }
}
}
# Confirmation gate
if not $yes {
print ""
let answer = (input $" Proceed with publish to ($svc.id)? [y/n] ")
if not ($answer | str starts-with "y") {
print " Publish cancelled."
return { service: $id, status: "cancelled" }
}
}
# Post-publish actions
mut post_ok = true
if ($post | is-not-empty) {
print ""
print " post_publish:"
$post_ok = (run-commands $post "post")
if not $post_ok {
print ""
print " WARNING: post_publish failed. Publish may have succeeded but follow-up actions incomplete."
}
}
let status = if $pre_ok and $post_ok { "published" } else { "partial" }
print ""
print $" Service '($id)' → ($status)"
{ service: $id, status: $status }
}
# List available publication services
export def "manifest publish list" [
--fmt: string = "table"
]: nothing -> table {
let m = (manifest load)
let services = ($m.publication_services? | default [])
let rows = ($services | each {|svc|
{
id: $svc.id,
artifact: ($svc.artifact? | default ""),
scope: ($svc.scope? | default ""),
auth: ($svc.auth_method? | default "None"),
trigger: ($svc.trigger? | default ""),
pre: ($svc.pre_publish? | default [] | length),
post: ($svc.post_publish? | default [] | length),
}
})
match $fmt {
"json" => { $rows | to json }
_ => { $rows }
}
}
# Show all layers with their visibility status
export def "manifest layers" [
--mode (-m): string = "" # Show visibility for a specific mode
]: nothing -> table {
let m = (manifest load)
let layers = ($m.layers? | default [])
if ($mode | is-not-empty) {
let op_mode = (find-mode $m $mode)
let visible = ($op_mode.visible_layers? | default [])
$layers | each {|l|
{
id: $l.id,
committed: $l.committed,
visible: ($l.id in $visible),
paths: ($l.paths | str join ", "),
description: ($l.description? | default ""),
}
}
} else {
$layers | each {|l|
{
id: $l.id,
committed: $l.committed,
paths: ($l.paths | str join ", "),
description: ($l.description? | default ""),
}
}
}
}
# Show consumption modes — who consumes and what they need
export def "manifest consumers" [
--fmt: string = "table"
]: nothing -> table {
let m = (manifest load)
let consumers = ($m.consumption_modes? | default [])
let rows = ($consumers | each {|c|
{
consumer: ($c.consumer? | default ""),
needs: ($c.needs? | default [] | str join ", "),
audit: ($c.audit_level? | default "Standard"),
description: ($c.description? | default ""),
}
})
match $fmt {
"json" => { $rows | to json }
_ => { $rows }
}
}

412
reflection/modules/nats.nu Normal file
View File

@ -0,0 +1,412 @@
#!/usr/bin/env nu
# nats.nu - Bidirectional NATS event system for reflection operations.
#
# Production-ready implementation with:
# - Payload validation against Nickel contracts
# - Robust event parsing from nats notify
# - Real handler loading + execution
# - Error handling + logging
# - Environment variable management
use ./store.nu *
# -- Configuration ────────────────────────────────────────────────────────────
def get-nats-config []: nothing -> record {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let config_path = $root + "/.ontoref/config.ncl"
if not ($config_path | path exists) {
return { nats_events: { enabled: false, url: "nats://localhost:4222", handlers_dir: "reflection/handlers" } }
}
let result = (do { ^nickel export $config_path } | complete)
if $result.exit_code == 0 {
$result.stdout | from json
} else {
{ nats_events: { enabled: false, url: "nats://localhost:4222", handlers_dir: "reflection/handlers" } }
}
}
def get-subjects []: nothing -> record {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let subjects_path = $root + "/nats/subjects.ncl"
let nickel_path = $root + ":/Users/Akasha/Tools/dev-system/ci/schemas"
if not ($subjects_path | path exists) {
return { subjects: {}, payloads: {} }
}
let result = (do {
env NICKEL_IMPORT_PATH=$nickel_path
^nickel export $subjects_path
} | complete)
if $result.exit_code == 0 {
$result.stdout | from json
} else {
{ subjects: {}, payloads: {} }
}
}
def event-to-contract-name [event: string]: nothing -> string {
match $event {
"mode.started" => "NushellModeStarted",
"mode.completed" => "NushellModeCompleted",
"mode.failed" => "NushellModeCompleted",
"sync.completed" => "NushellSyncCompleted",
"config.changed" => "NushellConfigChanged",
"reload" => "NushellReload",
_ => ""
}
}
# -- Availability Check ───────────────────────────────────────────────────────
export def "nats-available" []: nothing -> bool {
if ($env.NATS_AVAILABLE? | is-not-empty) {
return ($env.NATS_AVAILABLE | into bool)
}
let config = (get-nats-config)
let enabled = ($config.nats_events.enabled? | default false)
if not $enabled {
$env.NATS_AVAILABLE = "false"
return false
}
let result = (do { nats status } | complete)
let available = ($result.exit_code == 0)
$env.NATS_AVAILABLE = ($available | into string)
$available
}
def nats-subject [event: string]: nothing -> string {
let subjects = (get-subjects)
let mapping = $subjects.subjects
match $event {
"mode.started" => { $mapping.nushell_mode_started? | default "" },
"mode.completed" => { $mapping.nushell_mode_completed? | default "" },
"mode.failed" => { $mapping.nushell_mode_failed? | default "" },
"sync.completed" => { $mapping.nushell_sync_completed? | default "" },
"config.changed" => { $mapping.nushell_config_changed? | default "" },
"reload" => { $mapping.nushell_reload? | default "" },
"reflection.request" => { $mapping.reflection_request? | default "" },
"ontology.changed" => { $mapping.ontology_validated? | default "" },
_ => { "" }
}
}
# Validate payload against schema field requirements
def validate-payload [event: string, payload: record]: nothing -> record {
let contract_name = (event-to-contract-name $event)
match $event {
"mode.started" => {
# Required: mode_id, project, actor, timestamp
if ($payload.mode_id? | is-empty) {
error make { msg: "Invalid NushellModeStarted: missing mode_id" }
}
if ($payload.project? | is-empty) {
error make { msg: "Invalid NushellModeStarted: missing project" }
}
if ($payload.actor? | is-empty) {
error make { msg: "Invalid NushellModeStarted: missing actor" }
}
if ($payload.timestamp? | is-empty) {
error make { msg: "Invalid NushellModeStarted: missing timestamp" }
}
},
"mode.completed" => {
# Required: mode_id, project, status, steps_run, timestamp
if ($payload.mode_id? | is-empty) {
error make { msg: "Invalid NushellModeCompleted: missing mode_id" }
}
if ($payload.project? | is-empty) {
error make { msg: "Invalid NushellModeCompleted: missing project" }
}
if ($payload.status? | is-empty) {
error make { msg: "Invalid NushellModeCompleted: missing status" }
}
if ($payload.steps_run? | is-empty) {
error make { msg: "Invalid NushellModeCompleted: missing steps_run" }
}
if ($payload.timestamp? | is-empty) {
error make { msg: "Invalid NushellModeCompleted: missing timestamp" }
}
},
"sync.completed" => {
# Required: project, nodes_ok, nodes_warn, nodes_err, timestamp
if ($payload.project? | is-empty) {
error make { msg: "Invalid NushellSyncCompleted: missing project" }
}
if ($payload.timestamp? | is-empty) {
error make { msg: "Invalid NushellSyncCompleted: missing timestamp" }
}
},
"reload" => {
# Required: reason, project
if ($payload.reason? | is-empty) {
error make { msg: "Invalid NushellReload: missing reason" }
}
},
_ => { }
}
$payload
}
# -- Emit ─────────────────────────────────────────────────────────────────────
export def "nats-emit" [event: string]: record -> nothing {
if not (nats-available) { return }
let subject = (nats-subject $event)
if ($subject | is-empty) {
let cross = (ansi red) + "✗" + (ansi reset)
print $" $cross Unknown event: ($event)"
return
}
let config = (get-nats-config)
let should_emit = ($config.nats_events.emit? | default [] | where { |e| $e == $event } | is-not-empty)
if not $should_emit {
print $" (ansi dark_gray)[nats-emit] ($event) not in emit list(ansi reset)"
return
}
let payload = $in
# Validate payload
let validated = (do {
validate-payload $event $payload
} | complete)
if $validated.exit_code != 0 {
print $" (ansi red)✗(ansi reset) Validation failed: ($validated.stderr? | default 'unknown error')"
return
}
# Publish to NATS
let nats_url = ($config.nats_events.url? | default "nats://localhost:4222")
let publish_result = (do {
with-env { NATS_SERVER: $nats_url } {
$payload | to json | ^nats pub $subject
}
} | complete)
if $publish_result.exit_code == 0 {
let check = (ansi green) + "✓" + (ansi reset)
print $" $check Event published: ($event) → ($subject)"
} else {
let err = ($publish_result.stderr? | default "unknown error")
let cross = (ansi red) + "✗" + (ansi reset)
print $" $cross Publish failed: $err"
}
}
# -- Listen ───────────────────────────────────────────────────────────────────
export def "nats-listen" [--interval: int = 2]: nothing -> nothing {
if not (nats-available) {
print " [nats-listen] NATS unavailable, skipping"
return
}
let config = (get-nats-config)
let nats_url = ($config.nats_events.url? | default "nats://localhost:4222")
let subscribe_list = ($config.nats_events.subscribe? | default [] | str join ", ")
let interval_text = ($interval | into string)
let cyan = (ansi cyan)
let gray = (ansi dark_gray)
let reset = (ansi reset)
print $" $cyan→$reset Listening for NATS events (interval: ($interval_text)s)"
print $" $graySubscribe to: ($subscribe_list)$reset"
print ""
# Main loop
loop {
let events_result = (do {
with-env { NATS_SERVER: $nats_url } {
^nats notify --count 50 --timeout $interval
}
} | complete)
if $events_result.exit_code == 0 and ($events_result.stdout | is-not-empty) {
# Parse nats notify output
# Format: [stream] [subject] [sequence] [timestamp] [payload]
let parsed = (parse-nats-events $events_result.stdout)
for event in $parsed {
let dispatch_result = (do {
dispatch-event $event.subject $event.payload
} | complete)
if $dispatch_result.exit_code != 0 {
let err = ($dispatch_result.stderr? | default "unknown error")
let red = (ansi red)
let reset = (ansi reset)
print $" $red[dispatch-error]$reset ($event.subject): $err"
}
}
} else if ($events_result.exit_code != 0) {
let err = ($events_result.stderr? | default "timeout or connection error")
let yellow = (ansi yellow)
let reset = (ansi reset)
print $" $yellow[listen-warn]$reset $err - retrying..."
}
let sleep_time = ($"($interval)s" | into duration)
sleep $sleep_time
}
}
def parse-nats-events [output: string]: nothing -> list {
let lines = ($output | split row "\n" | where { |line| $line | is-not-empty })
if ($lines | is-empty) { return [] }
# Skip header line if present
let data_lines = if ($lines.0? | str starts-with "Stream") {
$lines | skip 1
} else {
$lines
}
# Parse each line: [stream] [subject] [sequence] [timestamp] [payload...]
$data_lines | each { |line|
let trimmed = ($line | str trim)
if ($trimmed | is-empty) { return null }
# Split on first occurrence of spaces, being careful about payloads
let parts = ($trimmed | split row " " | where { |p| $p != "" })
if ($parts | length) < 3 {
return null
}
let stream = $parts.0?
let subject = $parts.1?
let sequence = $parts.2?
let timestamp = $parts.3?
# Remaining parts are the payload (might be JSON or empty)
let payload_str = (
$parts
| skip 4
| str join " "
)
let payload = (
if ($payload_str | is-empty) {
{}
} else {
do {
$payload_str | from json
} | complete | if $env._.exit_code == 0 { $env._.stdout } else { {} }
}
)
{
stream: $stream,
subject: $subject,
sequence: $sequence,
timestamp: $timestamp,
payload: $payload,
}
} | where { |e| $e != null }
}
def dispatch-event [subject: string, payload: record]: nothing -> nothing {
let config = (get-nats-config)
let handlers_dir = ($config.nats_events.handlers_dir? | default "reflection/handlers")
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
# Map subject to handler file
let handler_path = match $subject {
"ecosystem.reflection.request" => {
$root + "/" + $handlers_dir + "/reflection-request.nu"
},
"ecosystem.ontology.changed" => {
$root + "/" + $handlers_dir + "/ontology-changed.nu"
},
"ecosystem.reflection.nushell.reload" => {
$root + "/" + $handlers_dir + "/reload.nu"
},
_ => { "" }
}
if ($handler_path | is-empty) {
let yellow = (ansi yellow)
let reset = (ansi reset)
print $" $yellow[dispatch]$reset No handler for ($subject)"
return
}
if not ($handler_path | path exists) {
error make { msg: $"Handler not found: ($handler_path)" }
}
# Execute handler using nu -c with payload JSON passed via environment
let payload_json = ($payload | to json)
let exec_result = (do {
env NATS_PAYLOAD=$payload_json
^nu $handler_path
} | complete)
if $exec_result.exit_code == 0 {
let check = (ansi green) + "✓" + (ansi reset)
print $" $check Handler executed: ($subject)"
} else {
let err = ($exec_result.stderr? | default "unknown error")
error make { msg: $"Handler failed for ($subject): $err" }
}
}
# -- Status ───────────────────────────────────────────────────────────────────
export def "nats-status" []: nothing -> nothing {
if not (nats-available) {
print " [NATS] unavailable"
return
}
let config = (get-nats-config)
let nats_server = ($config.nats_events.url? | default "nats://localhost:4222")
let enabled = ($config.nats_events.enabled? | default false)
let emit_count = ($config.nats_events.emit? | default [] | length)
let subscribe_count = ($config.nats_events.subscribe? | default [] | length)
print ""
let cyan = (ansi cyan)
let gray = (ansi dark_gray)
let reset = (ansi reset)
print $" $cyanNATS Event System$reset"
print $" $gray────────────────────────────────────────$reset"
print $" Server: $nats_server"
print $" Enabled: $enabled"
print $" Emit events: $emit_count"
print $" Subscriptions: $subscribe_count"
let status_result = (do {
with-env { NATS_SERVER: $nats_server } {
^nats status
}
} | complete)
if $status_result.exit_code == 0 {
print ""
print $status_result.stdout
} else {
print ""
let red = (ansi red)
let cross = $red + "✗" + $reset
print $" $cross Could not reach NATS server"
}
print ""
}

View File

@ -0,0 +1,47 @@
#!/usr/bin/env nu
# reflection/modules/prereqs.nu — prerequisite checking.
use env.nu *
export def "prereqs check" [
--context: string = "",
--form: string = "",
--severity: string = "",
--json,
] {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let ctx = if ($context | is-not-empty) { $context } else { actor-to-context $actor }
let checker = $"($env.ONTOREF_ROOT)/reflection/bin/check-prereqs.nu"
let args = (
["--context", $ctx]
| append (if ($form | is-not-empty) { ["--form", $form] } else { [] })
| append (if ($severity | is-not-empty) { ["--severity", $severity] } else { [] })
| append (if $json { ["--json"] } else { [] })
)
^nu $checker ...$args
}
export def "prereqs help" [] {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
print ""
print "Prerequisites commands:"
print " prereqs check check for current actor context"
print " prereqs check --context ci check CI context"
print " prereqs check --context agent check agent context"
print " prereqs check --form new_adr check what a specific form needs"
print " prereqs check --severity Hard only Hard requirements"
print " prereqs check --json machine-readable output"
print ""
print $"Current actor: ($actor) → context: (actor-to-context $actor)"
print ""
}
def actor-to-context [actor: string] {
match $actor {
"ci" => "ci",
"agent" => "agent",
"benchmark" => "benchmark",
_ => "local_dev",
}
}

View File

@ -0,0 +1,248 @@
#!/usr/bin/env nu
# reflection/modules/register.nu — ontoref register implementation.
#
# Reads a structured change record (from register_change.ncl form output or
# piped JSON) and writes the appropriate artifacts:
# - CHANGELOG entry (always)
# - ADR hint (if needs_adr = true — tells user the /create-adr command)
# - .ontology/state.ncl patch (if changes_ontology_state = true)
# - reflection/modes/<id>.ncl stub (if affects_capability + add)
#
# Ontology patch uses sed range pattern — no mutable closures.
# All modified files are nickel-typechecked before write.
use env.nu *
use store.nu [daemon-export-safe]
export def "register run" [
--backend: string = "cli",
] {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let form_file = $"($env.ONTOREF_ROOT)/reflection/forms/register_change.ncl"
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
if $actor == "agent" {
print "Agent: pipe a JSON record matching register_change.ncl fields to 'register apply'"
print $" nickel export ($form_file) | get elements"
print " | where type != \"section_header\" | select name prompt required nickel_path"
return
}
if (which typedialog | is-empty) {
print "typedialog required for interactive register. Run: cargo install typedialog"
exit 1
}
let tmp_out = (mktemp --suffix ".json")
match $backend {
"tui" => { ^typedialog tui nickel-roundtrip $form_file $form_file --output $tmp_out }
"web" => { ^typedialog web nickel-roundtrip $form_file $form_file --output $tmp_out --open }
_ => { ^typedialog nickel-roundtrip $form_file $form_file --output $tmp_out }
}
if not ($tmp_out | path exists) {
print "register cancelled — no output written"
return
}
let data = open $tmp_out | from json
rm $tmp_out
$data | register apply
}
# Apply a structured change record. Pipe JSON or call directly.
export def "register apply" [] {
let data = $in
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
if ($data.ready? | default true) == false {
print "register: ready = false — no artifacts written"
return
}
print ""
print $"ontoref register — ($data.summary)"
print "────────────────────────────────────────────────"
# ── CHANGELOG ───────────────────────────────────────────────────────────────
let changelog = $"($root)/CHANGELOG.md"
if ($changelog | path exists) {
register-changelog $data $changelog
} else {
print $" skip : CHANGELOG.md not found at ($changelog)"
}
# ── ADR hint ─────────────────────────────────────────────────────────────────
if ($data.needs_adr? | default false) {
let title = ($data.adr_title? | default "")
if ($title | str length) > 0 {
let flag = if ($data.adr_accept_immediately? | default false) { "-a " } else { "" }
print $" adr : run in a Claude session:"
print $" /create-adr ($flag)\"($title)\""
}
}
# ── ONTOLOGY STATE ──────────────────────────────────────────────────────────
if ($data.changes_ontology_state? | default false) {
let dim_id = ($data.ontology_dimension_id? | default "")
let new_state = ($data.ontology_new_state? | default "")
if ($dim_id | str length) > 0 and ($new_state | str length) > 0 {
register-ontology-state $root $dim_id $new_state
} else {
print " skip : ontology — dimension_id or new_state missing"
}
}
# ── MODE STUB ───────────────────────────────────────────────────────────────
if ($data.affects_capability? | default false) {
let action = ($data.capability_action? | default "add")
let mode_id = ($data.capability_mode_id? | default "")
if $action == "add" and ($mode_id | str length) > 0 {
register-mode-stub $root $mode_id $data.summary
} else if $action != "add" {
print $" mode : ($action) '($mode_id)' — edit reflection/modes/($mode_id).ncl manually"
} else {
print " skip : mode — capability_mode_id missing"
}
}
# ── CONFIG SEAL ─────────────────────────────────────────────────────────────
if ($data.seals_config_profile? | default false) {
let profile = ($data.config_profile? | default "")
if ($profile | str length) > 0 {
let adr_ref = ($data.adr_title? | default "")
let note_ref = ($data.summary? | default "")
print $" config : sealing profile '($profile)' via ontoref config apply"
let entry_bin = $"($root)/onref"
do { ^$entry_bin config apply $profile --note $note_ref } | complete | ignore
} else {
print " skip : config seal — config_profile missing"
}
}
print ""
print "Done. Review written artifacts before committing."
print ""
}
# ── Internal ────────────────────────────────────────────────────────────────────
def changelog-section [change_type: string]: nothing -> string {
match $change_type {
"feature" => "Added",
"fix" => "Fixed",
"architectural" => "Changed",
"refactor" => "Changed",
"tooling" => "Changed",
"docs" => "Changed",
"config" => "Changed",
_ => "Changed",
}
}
def register-changelog [data: record, changelog: string] {
let section = (changelog-section ($data.change_type? | default "feature"))
let detail = ($data.detail? | default "")
let detail_block = if ($detail | str length) > 0 {
let indented = ($detail | str trim | lines | each { |l| $" ($l)" } | str join "\n")
$"\n($indented)"
} else {
""
}
let entry = $"- **($data.summary)**($detail_block)"
let content = open $changelog
let updated = if ($content | str contains $"### ($section)") {
$content | str replace $"### ($section)" $"### ($section)\n\n($entry)"
} else {
$content | str replace "## [Unreleased]" $"## [Unreleased]\n\n### ($section)\n\n($entry)"
}
$updated | save --force $changelog
print $" changelog: ($section) — ($data.summary)"
}
def register-ontology-state [root: string, dim_id: string, new_state: string] {
let state_file = $"($root)/.ontology/state.ncl"
if not ($state_file | path exists) {
print $" skip : ($state_file) not found"
return
}
let state_data = (daemon-export-safe $state_file)
if $state_data == null {
print $" error : cannot export ($state_file)"
return
}
let dims = ($state_data | get dimensions)
if not ($dims | any { |d| $d.id == $dim_id }) {
print $" skip : dimension '($dim_id)' not found in state.ncl"
return
}
# sed range: from line matching id = "dim_id" to the closing brace of that block,
# replace the first occurrence of current_state = "...".
# macOS sed uses '' for -i; GNU sed uses -i without arg — try macOS first.
let sed_script = $"/id.*=.*\"($dim_id)\"/,/},/ s/current_state[[:space:]]*=[[:space:]]*\"[^\"]*\"/current_state = \"($new_state)\"/"
let sed_result = do { ^sed -i '' $sed_script $state_file } | complete
let sed_result = if $sed_result.exit_code != 0 {
do { ^sed -i $sed_script $state_file } | complete
} else {
$sed_result
}
if $sed_result.exit_code != 0 {
print $" error : sed patch failed — edit ($state_file) manually"
return
}
let typecheck = do { ^nickel typecheck $state_file } | complete
if $typecheck.exit_code != 0 {
print $" error : typecheck failed after patch — reverting"
print $typecheck.stderr
do { ^git checkout -- $state_file } | complete | ignore
return
}
print $" ontology: ($dim_id) → ($new_state)"
}
def register-mode-stub [root: string, mode_id: string, summary: string] {
let modes_dir = $"($root)/reflection/modes"
let mode_file = $"($modes_dir)/($mode_id).ncl"
if ($mode_file | path exists) {
print $" skip : ($mode_id).ncl already exists — edit manually"
return
}
if not ($modes_dir | path exists) {
^mkdir -p $modes_dir
}
let header = "let s = import \"../schema.ncl\" in\n\n"
let body = $"{
id = \"($mode_id)\",
trigger = \"($summary)\",
preconditions = [],
steps = [
{
id = \"step-1\",
action = \"describe the action\",
actor = 'Both,
cmd = \"\",
verify = \"\",
on_error = { strategy = 'Stop },
},
],
postconditions = [],
} | s.Mode std.string.NonEmpty\n"
let stub = $header + $body
$stub | save --force $mode_file
print $" mode : stub written at reflection/modes/($mode_id).ncl — fill steps"
}

View File

@ -0,0 +1,347 @@
#!/usr/bin/env nu
# services.nu - Config-driven service lifecycle management with dependency resolution.
#
# Services configured in .ontoref/config.ncl:
# services.services[] - array of {id, enabled, depends_on[], config}
# services.startup_order - explicit startup order (optional)
# services.shutdown_order - explicit shutdown order (optional)
#
# Usage:
# ontoref services # Show all services status
# ontoref services start [id] # Start service (and dependencies)
# ontoref services stop [id] # Stop service
# ontoref services restart [id] # Restart service
# ontoref services status [id] # Show service status
# ontoref services health [id] # Health check service
use ../modules/store.nu *
# -- Configuration ------------------------------------------------------------
def get-project-config []: nothing -> record {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let config_path = $"($root)/.ontoref/config.ncl"
if not ($config_path | path exists) {
return { services: { services: [] }, daemon: {} }
}
let result = (do { ^nickel export $config_path } | complete)
if $result.exit_code == 0 {
$result.stdout | from json
} else {
{ services: { services: [] }, daemon: {} }
}
}
def get-internal-services []: nothing -> list {
# Only daemon is managed by onref. DB is external.
[
{ id: "daemon", enabled: false, depends_on: [], managed: true }
]
}
def get-services-list []: nothing -> list {
let config = (get-project-config)
let all = ($config.services.services? | default [])
# Filter to only managed services (daemon)
$all | where { |s| $s.id == "daemon" }
}
def get-service [id: string]: nothing -> record {
let services = (get-services-list)
let service = ($services | where id == $id | first)
if ($service | is-empty) {
error make { msg: $"Service not found: ($id)" }
}
$service
}
def get-startup-order []: nothing -> list {
let config = (get-project-config)
let explicit = ($config.services.startup_order? | default null)
if ($explicit | is-not-empty) {
$explicit
} else {
# Default order: daemon first, then db
["daemon", "db"]
}
}
def get-shutdown-order []: nothing -> list {
let config = (get-project-config)
let explicit = ($config.services.shutdown_order? | default null)
if ($explicit | is-not-empty) {
$explicit
} else {
# Reverse order: db first, then daemon
["db", "daemon"]
}
}
def daemon-pid-file []: nothing -> string {
$"($env.HOME)/.ontoref/daemon.pid"
}
def daemon-running? []: nothing -> bool {
let pid_file = (daemon-pid-file)
if not ($pid_file | path exists) { return false }
let pid = (open $pid_file | str trim)
let result = (do { ^kill -0 $pid } | complete)
$result.exit_code == 0
}
# -- Status -------------------------------------------------------------------
def "services" [action?: string, id?: string]: nothing -> nothing {
match ($action | default "") {
"" => { services overview },
"start" => { services start $id },
"stop" => { services stop $id },
"restart" => { services restart $id },
"status" => { services status $id },
"health" => { services health $id },
_ => { print $"Unknown action: ($action). Use: start, stop, restart, status, health" }
}
}
export def "main" [action?: string, id?: string]: nothing -> nothing {
services $action $id
}
export def "services overview" []: nothing -> nothing {
let config = (get-project-config)
let services = (get-services-list)
# Color codes (defined once)
let cyan = (ansi cyan)
let blue = (ansi blue)
let green = (ansi green)
let red = (ansi red)
let gray = (ansi dark_gray)
let yellow = (ansi yellow)
let reset = (ansi reset)
print ""
print ($cyan + " ontoref services (managed by ontoref)" + $reset)
print ($gray + " ----------------------------------------" + $reset)
print ""
# Show managed services (daemon)
for service in $services {
let status_text = if (daemon-running?) { "running" } else { "stopped" }
let status_icon = if ($status_text == "running") { "✓" } else { "✗" }
if $service.enabled {
let port = ($config.daemon.port? | default 7891)
let status_color = if ($status_text == "running") { $green } else { $red }
let line = $blue + " " + $service.id + $reset + " [" + $status_color + $status_icon + $reset + "] " + $status_text + " - port " + $yellow + ($port | into string) + $reset
print $line
} else {
let line = $blue + " " + $service.id + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset
print $line
}
}
print ""
print ($cyan + " External services (monitored only)" + $reset)
print ($gray + " ----------------------------------------" + $reset)
print ""
# Show external services: DB and NATS
let db_config = ($config.services.services? | default [] | where id == "db" | first)
if ($db_config | is-not-empty) {
if ($db_config.enabled | default false) {
let db_url = ($config.db.url? | default "")
if ($db_url | is-not-empty) {
let line = $blue + " db" + $reset + " " + $gray + "[status check only]" + $reset + " " + $yellow + $db_url + $reset
print $line
} else {
let line = $blue + " db" + $reset + " " + $gray + "[disabled]" + $reset + " no URL configured"
print $line
}
} else {
let line = $blue + " db" + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset
print $line
}
}
# NATS status
let nats_config = ($config.nats_events? | default { enabled: false, url: "" })
if ($nats_config.enabled | default false) {
let nats_url = ($nats_config.url? | default "nats://localhost:4222")
let line = $blue + " nats" + $reset + " " + $gray + "[event system]" + $reset + " " + $yellow + $nats_url + $reset
print $line
} else {
let line = $blue + " nats" + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset
print $line
}
print ""
print ($gray + " Manage: ontoref services <start|stop|restart> daemon" + $reset)
print ($gray + " Monitor: ontoref services <status|health> [daemon|db]" + $reset)
print ($gray + " Events: strat nats <status|listen|emit>" + $reset)
print ""
}
# -- Lifecycle Management -----------------------------------------------------
export def "services start" [id?: string]: nothing -> nothing {
let requested = ($id | default "daemon")
match $requested {
"daemon" => {
if (daemon-running?) {
print " ✓ daemon already running"
} else {
daemon-start
}
},
"db" => { print " [ext] database is external - manage separately" },
_ => { print $" [warn] unknown service: ($requested)" }
}
}
export def "services stop" [id?: string]: nothing -> nothing {
let requested = ($id | default "daemon")
match $requested {
"daemon" => { daemon-stop },
"db" => { print " [ext] database must be stopped externally" },
_ => { print $" [warn] unknown service: ($requested)" }
}
}
export def "services restart" [id?: string]: nothing -> nothing {
let requested = ($id | default "daemon")
match $requested {
"daemon" => {
print " Restarting daemon..."
daemon-stop
sleep 500ms
daemon-start
},
"db" => { print " [ext] database must be restarted externally" },
_ => { print $" [warn] unknown service: ($requested)" }
}
}
export def "services status" [id?: string]: nothing -> nothing {
if ($id | is-not-empty) {
match $id {
"daemon" => {
print ""
if (daemon-running?) {
let pid_file = (daemon-pid-file)
let pid = (open $pid_file | str trim)
print $" daemon: running (PID $pid)"
} else {
print " daemon: stopped"
}
print ""
},
"db" => { print " database: check externally" },
_ => { print $" unknown service: ($id)" }
}
} else {
services overview
}
}
export def "services health" [id?: string]: nothing -> nothing {
if ($id | is-not-empty) {
match $id {
"daemon" => { daemon-health },
"db" => { db-health },
_ => { print $" unknown service: ($id)" }
}
} else {
let services = (get-services-list | get id)
for svc_id in $services {
match $svc_id {
"daemon" => { daemon-health },
"db" => { db-health },
_ => {}
}
}
}
}
# -- Built-in Service Handlers ------------------------------------------------
def daemon-start []: nothing -> nothing {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let pid_file = (daemon-pid-file)
print " Starting daemon..."
mkdir ($pid_file | path dirname)
do { \^ontoref-daemon --project-root $root --pid-file $pid_file } &
sleep 500ms
if (daemon-running?) {
print " ✓ daemon started"
} else {
print " ✗ failed to start daemon"
}
}
def daemon-stop []: nothing -> nothing {
if not (daemon-running?) {
print " ✓ daemon not running"
return
}
let pid_file = (daemon-pid-file)
let pid = (open $pid_file | str trim)
print " Stopping daemon..."
do { ^kill $pid } | complete
sleep 500ms
if not (daemon-running?) {
print " ✓ daemon stopped"
rm -f $pid_file
} else {
do { ^kill -9 $pid } | complete
rm -f $pid_file
}
}
def daemon-health []: nothing -> nothing {
if not (daemon-running?) {
print " ✗ daemon not running"
return
}
let url = ($env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891")
let result = (do { ^curl -sf $"($url)/health" } | complete)
if $result.exit_code != 0 {
print " ✗ daemon health check failed"
return
}
let health = ($result.stdout | from json)
print $" ✓ daemon healthy (uptime: ($health.uptime_secs)s, cache: ($health.cache_entries) entries)"
}
def db-health []: nothing -> nothing {
let config = (get-project-config)
let db_config = ($config.services.services? | default [] | where id == "db" | first)
if ($db_config.config.url? | default "" | is-empty) {
print " ⚠ database URL not configured"
return
}
let url = $db_config.config.url
let result = (do { ^curl -sf $url } | complete)
if $result.exit_code == 0 {
print $" ✓ database healthy ($url)"
} else {
print $" ✗ database check failed ($url)"
}
}

306
reflection/modules/store.nu Normal file
View File

@ -0,0 +1,306 @@
#!/usr/bin/env nu
# store.nu — Nushell client for ontoref-daemon HTTP API.
#
# Provides cached nickel export via daemon (with subprocess fallback),
# plus query/sync/nodes/dimensions when daemon has DB enabled.
#
# All HTTP calls use ^curl (external command) because Nushell's internal
# http get/post cannot be captured with `| complete` on connection errors.
#
# Usage:
# use ../modules/store.nu *
#
# daemon-export ".ontology/core.ncl" # cached export
# daemon-export ".ontology/core.ncl" --import-path $ip # with NICKEL_IMPORT_PATH
# store nodes --level Axiom # query ontology nodes
# store dimensions # query dimensions
# daemon-health # check daemon status
# ── Utilities (self-contained to avoid circular imports with shared.nu) ─────────
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 nickel-import-path [root: string]: nothing -> string {
let entries = [
$"($root)/.ontology"
$"($root)/adrs"
$"($root)/.ontoref/ontology/schemas"
$"($root)/.ontoref/adrs"
$"($root)/.onref"
$root
$"($env.ONTOREF_ROOT)/ontology"
$"($env.ONTOREF_ROOT)/ontology/schemas"
$"($env.ONTOREF_ROOT)/adrs"
$env.ONTOREF_ROOT
]
let valid = ($entries | where { |p| $p | path exists } | uniq)
let existing = ($env.NICKEL_IMPORT_PATH? | default "")
if ($existing | is-not-empty) {
($valid | append $existing) | str join ":"
} else {
$valid | str join ":"
}
}
# ── Configuration ────────────────────────────────────────────────────────────────
def daemon-url []: nothing -> string {
$env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891"
}
# Load project config to check if DB is enabled
def project-config-db-status []: nothing -> record<enabled: bool, url: string, namespace: string> {
let root = (project-root)
let config_path = $"($root)/.ontoref/config.ncl"
if not ($config_path | path exists) {
return { enabled: false, url: "", namespace: "" }
}
# Export config and extract db section
let result = (do { ^nickel export $config_path } | complete)
if $result.exit_code != 0 {
return { enabled: false, url: "", namespace: "" }
}
# Parse JSON safely
let parse_result = (do { $result.stdout | from json } | complete)
if $parse_result.exit_code != 0 {
return { enabled: false, url: "", namespace: "" }
}
let config = $parse_result.stdout
let db = ($config.db? | default {})
{
enabled: ($db.enabled? | default false),
url: ($db.url? | default ""),
namespace: ($db.namespace? | default "ontoref"),
}
}
# ── HTTP helpers (external ^curl) ────────────────────────────────────────────────
def http-get [url: string]: nothing -> record {
do { ^curl -sf $url } | complete
}
def http-post-json [url: string, body: string]: nothing -> record {
do { ^curl -sf -X POST -H "Content-Type: application/json" -d $body $url } | complete
}
# ── Availability check ───────────────────────────────────────────────────────────
# Check if daemon is reachable. Caches "true" in env var for the session.
# A "false" result is NOT cached — allows recovery when daemon starts mid-session.
export def --env daemon-available []: nothing -> bool {
let cached = ($env.ONTOREF_DAEMON_AVAILABLE? | default "")
if $cached == "true" { return true }
let url = $"(daemon-url)/health"
let result = (http-get $url)
if $result.exit_code == 0 {
$env.ONTOREF_DAEMON_AVAILABLE = "true"
true
} else {
false
}
}
# ── Health ───────────────────────────────────────────────────────────────────────
# Show daemon health status with optional DB info from config.
# Returns record with { status, uptime_secs, cache_*, db_enabled?, db_config? }
# or null if daemon unreachable or response is not valid JSON.
export def daemon-health []: nothing -> any {
let url = $"(daemon-url)/health"
let result = (http-get $url)
if $result.exit_code != 0 {
null
} else {
let body = ($result.stdout | str trim)
if not ($body | str starts-with "{") {
null
} else {
let parse_result = (do { $body | from json } | complete)
if $parse_result.exit_code != 0 {
null
} else {
let health = $parse_result.stdout
let db_config = (project-config-db-status)
$health | insert db_config $db_config
}
}
}
}
# ── NCL Export (core function) ───────────────────────────────────────────────────
# Export a Nickel file to JSON via daemon (cached) with subprocess fallback.
#
# When daemon is available: POST /nickel/export → cached result.
# When daemon is unreachable: falls back to ^nickel export subprocess.
# System works identically either way — just slower without daemon.
export def --env daemon-export [
file: string,
--import-path: string = "",
]: nothing -> any {
let ip = if ($import_path | is-not-empty) {
$import_path
} else {
nickel-import-path (project-root)
}
if (daemon-available) {
let result = (daemon-export-http $file $ip)
if $result != null { return $result }
# HTTP call failed despite health check — clear cache to re-probe next call
$env.ONTOREF_DAEMON_AVAILABLE = ""
}
daemon-export-subprocess $file $ip
}
# Safe version: returns null on failure instead of throwing.
# Use for call sites that handle missing data gracefully (return [] or {}).
export def --env daemon-export-safe [
file: string,
--import-path: string = "",
]: nothing -> any {
if not ($file | path exists) { return null }
let ip = if ($import_path | is-not-empty) {
$import_path
} else {
nickel-import-path (project-root)
}
if (daemon-available) {
let result = (daemon-export-http $file $ip)
if $result != null { return $result }
$env.ONTOREF_DAEMON_AVAILABLE = ""
}
let result = do { with-env { NICKEL_IMPORT_PATH: $ip } { ^nickel export $file } } | complete
if $result.exit_code != 0 { return null }
$result.stdout | from json
}
# ── Daemon HTTP calls ────────────────────────────────────────────────────────────
def daemon-export-http [file: string, import_path: string]: nothing -> any {
let url = $"(daemon-url)/nickel/export"
let body = if ($import_path | is-not-empty) {
{ path: $file, import_path: $import_path } | to json
} else {
{ path: $file } | to json
}
let result = (http-post-json $url $body)
if $result.exit_code != 0 { return null }
let response = ($result.stdout | from json)
$response.data? | default null
}
# ── Subprocess fallback ──────────────────────────────────────────────────────────
def daemon-export-subprocess [file: string, import_path: string]: nothing -> any {
let ip = if ($import_path | is-not-empty) {
$import_path
} else {
let root = (project-root)
nickel-import-path $root
}
let result = do { with-env { NICKEL_IMPORT_PATH: $ip } { ^nickel export $file } } | complete
if $result.exit_code != 0 {
error make { msg: $"nickel export failed for ($file): ($result.stderr)" }
}
$result.stdout | from json
}
# ── Store query commands ─────────────────────────────────────────────────────────
# Query ontology nodes. Returns empty list on failure.
export def --env "store nodes" [
--level: string = "",
]: nothing -> list {
let root = (project-root)
let core_file = $"($root)/.ontology/core.ncl"
if not ($core_file | path exists) { return [] }
let data = (daemon-export-safe $core_file)
if $data == null { return [] }
let nodes = ($data.nodes? | default [])
if ($level | is-not-empty) {
$nodes | where { |n| ($n.level? | default "") == $level }
} else {
$nodes
}
}
# Query ontology dimensions. Returns empty list on failure.
export def --env "store dimensions" []: nothing -> list {
let root = (project-root)
let state_file = $"($root)/.ontology/state.ncl"
if not ($state_file | path exists) { return [] }
let data = (daemon-export-safe $state_file)
if $data == null { return [] }
$data.dimensions? | default []
}
# Query membranes. Returns empty list on failure.
export def --env "store membranes" [
--all = false,
]: nothing -> list {
let root = (project-root)
let gate_file = $"($root)/.ontology/gate.ncl"
if not ($gate_file | path exists) { return [] }
let data = (daemon-export-safe $gate_file)
if $data == null { return [] }
let membranes = ($data.membranes? | default [])
if $all {
$membranes
} else {
$membranes | where { |m| ($m.active? | default false) == true }
}
}
# ── Cache management ─────────────────────────────────────────────────────────────
# Show daemon cache statistics.
export def --env "store cache-stats" []: nothing -> any {
if not (daemon-available) {
print " daemon not available"
return null
}
let url = $"(daemon-url)/cache/stats"
let result = (http-get $url)
if $result.exit_code == 0 {
$result.stdout | from json
} else {
null
}
}
# Invalidate daemon cache (all entries or by prefix/file).
export def --env "store cache-invalidate" [
--prefix: string = "",
--file: string = "",
--all = false,
]: nothing -> any {
if not (daemon-available) {
print " daemon not available"
return null
}
let url = $"(daemon-url)/cache/invalidate"
if (not $all) and ($prefix | is-empty) and ($file | is-empty) {
error make { msg: "cache-invalidate requires --all, --prefix, or --file" }
}
let body = if $all {
{ all: true } | to json
} else if ($prefix | is-not-empty) {
{ prefix: $prefix } | to json
} else {
{ file: $file } | to json
}
let result = (http-post-json $url $body)
if $result.exit_code == 0 {
$result.stdout | from json
} else {
null
}
}

1370
reflection/modules/sync.nu Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,267 @@
# reflection/nulib/dashboard.nu — overview, status, health commands.
use ./fmt.nu *
use ./shared.nu [project-root, load-dimensions, load-gates, adrs-brief]
export def run-overview [fmt_resolved: string] {
let root = project-root
let name = ($root | path basename)
let cargo_toml = $"($root)/Cargo.toml"
let has_workspace = ($cargo_toml | path exists) and ((open $cargo_toml | get -o workspace | is-not-empty) == true)
let crates = if ($cargo_toml | path exists) {
let cargo = (open $cargo_toml)
let members = ($cargo | get -o workspace.members | default [])
if ($members | is-empty) {
let pkg_name = ($cargo | get -o package.name | default $name)
let deps = ($cargo | get -o dependencies | default {} | columns)
let features = ($cargo | get -o features | default {} | columns)
[{ name: $pkg_name, deps: ($deps | length), features: $features }]
} else {
mut result = []
for member in $members {
let expanded = (glob $"($root)/($member)/Cargo.toml")
for ct in $expanded {
let c = (open $ct)
let cname = ($c | get -o package.name | default ($ct | path dirname | path basename))
let deps = ($c | get -o dependencies | default {} | columns)
let features = ($c | get -o features | default {} | columns)
$result = ($result | append { name: $cname, deps: ($deps | length), features: $features })
}
}
$result
}
} else { [] }
let total_deps = ($crates | get deps | math sum)
let systems = [
(if ($"($root)/.ontology/core.ncl" | path exists) { "ontology" } else { null }),
(if ((glob $"($root)/adrs/adr-*.ncl" | length) > 0) { "ADRs" } else { null }),
(if ($"($root)/reflection" | path exists) { "reflection" } else { null }),
(if ($"($root)/.ontology/manifest.ncl" | path exists) { "manifest" } else { null }),
(if ($"($root)/.coder" | path exists) { ".coder" } else { null }),
(if ($"($root)/.claude/CLAUDE.md" | path exists) { ".claude" } else { null }),
(if ($"($root)/justfile" | path exists) { "justfile" } else { null }),
] | compact
let report = (sync audit --quick --fmt silent)
let health = ($report.health? | default 0.0)
let nodes = ($report.nodes? | default {})
let dims = (load-dimensions $root)
let reached = ($dims | where reached == true | length)
let total_dims = ($dims | length)
let brief = adrs-brief
let modes_count = ((glob $"($env.ONTOREF_ROOT)/reflection/modes/*.ncl" | length) + (if $root != $env.ONTOREF_ROOT { glob $"($root)/reflection/modes/*.ncl" | length } else { 0 }))
let just_count = if ($"($root)/justfiles" | path exists) { glob $"($root)/justfiles/*.just" | length } else { 0 }
let overview_data = {
project: $name,
root: $root,
workspace: $has_workspace,
crates: ($crates | each { |c| { name: $c.name, deps: $c.deps, features: ($c.features | str join ", ") } }),
total_crates: ($crates | length),
total_deps: $total_deps,
systems: $systems,
health: $health,
nodes: $nodes,
dimensions: { reached: $reached, total: $total_dims },
adrs: $brief,
modes: $modes_count,
just_modules: $just_count,
}
match $fmt_resolved {
"json" => { print ($overview_data | to json); return },
"yaml" => { print ($overview_data | to yaml); return },
"table" => { print ($overview_data | table --expand); return },
"toml" => {
print ($overview_data | to json)
return
},
_ => {},
}
let h_color = if $health >= 90.0 { (ansi green_bold) } else if $health >= 70.0 { (ansi yellow_bold) } else { (ansi red_bold) }
let bar_len = (($health / 5.0) | math round | into int)
let bar_fill = ("█" | fill -c "█" -w $bar_len)
let bar_empty = ("░" | fill -c "░" -w (20 - $bar_len))
print ""
fmt-header $name
fmt-sep
let ws_label = if $has_workspace { $"(ansi cyan)workspace(ansi reset)" } else { $"(ansi dark_gray)single crate(ansi reset)" }
print $" ($ws_label) (ansi dark_gray)($systems | str join ' · ')(ansi reset)"
print ""
print $" ($h_color)($bar_fill)(ansi dark_gray)($bar_empty)(ansi reset) ($h_color)($health)%(ansi reset)"
print ""
if ($crates | is-not-empty) {
let crate_label = $"(ansi white_bold)Crates(ansi reset) ($crates | length) (ansi dark_gray)total deps: ($total_deps)(ansi reset)"
print $" ($crate_label)"
for c in $crates {
let feat_str = if ($c.features | is-not-empty) { $" (ansi dark_gray)features: ($c.features | str join ', ')(ansi reset)" } else { "" }
let dep_count = $c.deps
print $" (ansi green)($c.name)(ansi reset) (ansi dark_gray)($dep_count) deps(ansi reset)($feat_str)"
}
print ""
}
let ok = ($nodes.ok? | default 0)
let issues = (($nodes.missing? | default 0) + ($nodes.stale? | default 0) + ($nodes.drift? | default 0))
let issues_str = if $issues > 0 { $"(ansi yellow)($issues)(ansi reset)" } else { $"(ansi dark_gray)0(ansi reset)" }
let adr_label = $"($brief.accepted)A/($brief.superseded)S/($brief.proposed)P"
let dim_label = if $total_dims > 0 { $"($reached)/($total_dims)" } else { "—" }
print $" (ansi white_bold)Nodes(ansi reset) (ansi green)($ok)(ansi reset) ok ($issues_str) issues (ansi white_bold)ADRs(ansi reset) (ansi cyan)($adr_label)(ansi reset) (ansi white_bold)Dims(ansi reset) (ansi cyan)($dim_label)(ansi reset) (ansi white_bold)Modes(ansi reset) ($modes_count) (ansi white_bold)Just(ansi reset) ($just_count)"
let active_dims = ($dims | where reached == false)
if ($active_dims | is-not-empty) {
print ""
for d in $active_dims {
print $" (ansi dark_gray)($d.id): ($d.current_state) → ($d.desired_state)(ansi reset)"
}
}
print ""
}
export def run-health [fmt_resolved: string, full: bool] {
let report = if $full {
sync audit --fmt (if $fmt_resolved == "json" { "json" } else { "silent" })
} else {
sync audit --quick --fmt (if $fmt_resolved == "json" { "json" } else { "silent" })
}
let health_data = {
health: ($report.health? | default 0.0),
nodes: ($report.nodes? | default {}),
}
match $fmt_resolved {
"json" => { print ($health_data | to json); return },
"yaml" => { print ($health_data | to yaml); return },
"toml" => { print ($health_data | to toml); return },
"table" => { print ($health_data | table --expand); return },
_ => {},
}
let health = $health_data.health
let nodes = $health_data.nodes
let ok = ($nodes.ok? | default 0)
let missing = ($nodes.missing? | default 0)
let stale = ($nodes.stale? | default 0)
let drift = ($nodes.drift? | default 0)
let broken = ($nodes.broken_edges? | default 0)
render-health-bar $health
print $" (ansi white_bold)Nodes(ansi reset) (ansi green)($ok) OK(ansi reset) (ansi dark_gray)($missing)M ($stale)S ($drift)D ($broken)B(ansi reset)"
print ""
}
export def run-status [fmt_resolved: string] {
let root = project-root
let name = ($root | path basename)
let report = (sync audit --quick --fmt silent)
let health = ($report.health? | default 0.0)
let dims = (load-dimensions $root)
let reached = ($dims | where reached == true | length)
let total_dims = ($dims | length)
let brief = adrs-brief
let adr_label = $"($brief.accepted)A/($brief.superseded)S/($brief.proposed)P"
let gates = (load-gates $root)
let nodes = ($report.nodes? | default {})
let ok = ($nodes.ok? | default 0)
let issues = (($nodes.missing? | default 0) + ($nodes.stale? | default 0) + ($nodes.drift? | default 0))
let recent = (coder log --limit 5)
let git_log = do { ^git -C $root log --oneline -3 } | complete
let commits = if $git_log.exit_code == 0 {
$git_log.stdout | lines | where { $in | is-not-empty }
} else { [] }
let status_data = {
project: $name,
root: $root,
health: $health,
nodes: { ok: $ok, issues: $issues },
dimensions: { reached: $reached, total: $total_dims },
adrs: $brief,
gates: ($gates | length),
recent_coder: ($recent | each { |r| { title: ($r.title? | default ""), kind: ($r.kind? | default ""), ts: ($r.timestamp? | default "") } }),
recent_commits: $commits,
}
match $fmt_resolved {
"json" => { print ($status_data | to json); return },
"yaml" => { print ($status_data | to yaml); return },
"toml" => {
print ($status_data | to json)
return
},
"table" => { print ($status_data | table --expand); return },
_ => {},
}
let h_color = if $health >= 90.0 { (ansi green_bold) } else if $health >= 70.0 { (ansi yellow_bold) } else { (ansi red_bold) }
let bar_len = (($health / 5.0) | math round | into int)
let bar_fill = ("█" | fill -c "█" -w $bar_len)
let bar_empty = ("░" | fill -c "░" -w (20 - $bar_len))
print ""
fmt-header $"($name) — project status"
fmt-sep
print $" (ansi white_bold)Health(ansi reset) ($h_color)($bar_fill)(ansi dark_gray)($bar_empty)(ansi reset) ($h_color)($health)%(ansi reset) (ansi dark_gray)\(quick)(ansi reset)"
let issues_str = if $issues > 0 { $"(ansi yellow)($issues) issues(ansi reset)" } else { $"(ansi dark_gray)0 issues(ansi reset)" }
print $" (ansi white_bold)Nodes(ansi reset) (ansi green)($ok) OK(ansi reset) ($issues_str)"
print $" (ansi white_bold)ADRs(ansi reset) (ansi cyan)($adr_label)(ansi reset)"
print $" (ansi white_bold)Gates(ansi reset) (ansi cyan)($gates | length) active(ansi reset)"
if $total_dims > 0 {
print $" (ansi white_bold)State(ansi reset) (ansi green)($reached)(ansi reset)/(ansi cyan)($total_dims)(ansi reset) dimensions reached"
let active_dims = ($dims | where reached == false)
for d in $active_dims {
print $" (ansi dark_gray)($d.id): ($d.current_state) → ($d.desired_state)(ansi reset)"
}
}
if ($commits | is-not-empty) {
print ""
fmt-section "Recent commits"
print ""
for c in $commits {
print $" (ansi dark_gray)($c)(ansi reset)"
}
}
if ($recent | is-not-empty) {
print ""
fmt-section "Recent activity (.coder/)"
print ""
for r in $recent {
let kind_color = match ($r.kind? | default "") {
"done" => (ansi green),
"plan" => (ansi cyan),
"review" => (ansi magenta),
"audit" => (ansi yellow),
_ => (ansi default_dimmed),
}
let ts = ($r.timestamp? | default "" | str substring 0..9)
print $" ($kind_color)($r.kind? | default "?")(ansi reset) (ansi dark_gray)($ts)(ansi reset) ($r.title? | default "")"
}
}
print ""
}

97
reflection/nulib/fmt.nu Normal file
View File

@ -0,0 +1,97 @@
# reflection/lib/fmt.nu — ANSI formatting primitives for the dispatcher UI.
# Shared by help, dashboard, and interactive systems.
export def fmt-header [text: string] {
print $"(ansi cyan_bold)($text)(ansi reset)"
}
export def fmt-sep [] {
print $"(ansi dark_gray)──────────────────────────────────────────────────────────────────(ansi reset)"
}
export def fmt-section [text: string] {
print $" (ansi white_bold)($text)(ansi reset)"
}
export def fmt-info [text: string] {
print $" (ansi default_dimmed)($text)(ansi reset)"
}
# Format a command line with the group verb highlighted.
# verb_pos: which word (0-indexed from after caller) to highlight.
export def fmt-cmd [cmd: string, desc: string = "", --verb-pos (-v): int = 0] {
let parts = ($cmd | split row " ")
let caller = ($env.ONTOREF_CALLER? | default "./onref")
let caller_parts = ($caller | split row " " | length)
let after = ($parts | skip $caller_parts)
let colored = if ($after | is-empty) {
$"(ansi green)($cmd)(ansi reset)"
} else {
mut segments = []
for i in 0..(($after | length) - 1) {
let word = ($after | get $i)
if $i == $verb_pos {
$segments = ($segments | append $"(ansi cyan_bold)($word)(ansi reset)")
} else {
$segments = ($segments | append $"(ansi green)($word)(ansi reset)")
}
}
let prefix = ($parts | first $caller_parts | str join " ")
$"(ansi green)($prefix)(ansi reset) ($segments | str join ' ')"
}
if ($desc | is-empty) {
print $" ($colored)"
} else {
print $" ($colored) (ansi dark_gray)($desc)(ansi reset)"
}
}
export def fmt-badge [text: string]: nothing -> string {
$"(ansi magenta_bold)[($text)](ansi reset)"
}
# Resolve --fmt: if "select" or "?", show interactive picker; otherwise pass through.
def expand-fmt-alias [fmt: string]: nothing -> string {
match $fmt {
"txt" | "tx" => "text",
"tab" | "ta" => "table",
"j" | "jsn" => "json",
"y" | "yml" => "yaml",
"t" | "tml" => "toml",
"m" => "md",
_ => $fmt,
}
}
export def resolve-fmt [fmt: string, choices: list<string>]: nothing -> string {
if $fmt == "select" or $fmt == "?" {
let picked = ($choices | input list $"(ansi cyan_bold)Format:(ansi reset) ")
if ($picked | is-empty) { $choices | first } else { $picked }
} else if ($fmt | is-empty) {
$choices | first
} else {
expand-fmt-alias $fmt
}
}
export def fmt-aliases [aliases: list<record>] {
fmt-section "ALIASES"
print ""
let max_short = ($aliases | each { |a| $a.short | str length } | math max)
for a in $aliases {
let pad = ($max_short - ($a.short | str length))
let spaces = ("" | fill -c " " -w $pad)
print $" (ansi cyan)($a.short)(ansi reset)($spaces) → ($a.long)"
}
print ""
}
export def render-health-bar [health: float] {
let color = if $health >= 90.0 { (ansi green_bold) } else if $health >= 70.0 { (ansi yellow_bold) } else { (ansi red_bold) }
let bar_len = (($health / 5.0) | math round | into int)
let bar_fill = ("█" | fill -c "█" -w $bar_len)
let bar_empty = ("░" | fill -c "░" -w (20 - $bar_len))
print ""
print $" ($color)($bar_fill)(ansi dark_gray)($bar_empty)(ansi reset) ($color)($health)%(ansi reset)"
print ""
}

376
reflection/nulib/help.nu Normal file
View File

@ -0,0 +1,376 @@
# reflection/nulib/help.nu — Help text rendering for all command groups.
use ./fmt.nu *
use ./shared.nu [all-mode-files, adrs-brief]
use ../modules/store.nu [daemon-export-safe]
use ../modules/forms.nu ["forms list"]
export def help-group [group: string] {
let cmd = ($env.ONTOREF_CALLER? | default "./onref")
let actor = ($env.ONTOREF_ACTOR? | default "developer")
match $group {
"check" | "prereqs" => {
print ""
fmt-header "PREREQUISITES"
fmt-sep
fmt-cmd $"($cmd) check" "check current actor context"
fmt-cmd $"($cmd) check --context ci" "check CI environment"
fmt-cmd $"($cmd) check --form <name>" "check what a specific form needs"
fmt-cmd $"($cmd) check --severity <s>" "filter by severity"
fmt-cmd $"($cmd) check --json" "machine-readable output"
print ""
fmt-aliases [
{ short: "ck", long: "check" },
]
},
"form" | "forms" => {
print ""
fmt-header "FORM"
fmt-sep
let form_items = (forms list)
for f in $form_items {
if $actor == "agent" {
fmt-cmd $"nickel export reflection/forms/($f.name).ncl"
fmt-info $f.description
} else {
fmt-cmd $"($cmd) form run ($f.name)" -v 1
fmt-cmd $"($cmd) form run ($f.name) --backend web" "browser UI" -v 1
fmt-cmd $"($cmd) form run ($f.name) --backend tui" "terminal 3-panel UI" -v 1
fmt-info $f.description
}
print ""
}
fmt-cmd $"($cmd) form list" "list all available forms"
fmt-cmd $"($cmd) form ls" "list forms (machine-friendly)" -v 1
print ""
fmt-aliases [
{ short: "fm", long: "form" },
{ short: "fm l", long: "form list" },
]
},
"mode" | "modes" => {
print ""
fmt-header "MODE (operational procedures)"
fmt-sep
fmt-section "Inspect"
print ""
let mode_files = (all-mode-files)
for f in $mode_files {
let m = (daemon-export-safe $f)
if $m != null {
fmt-cmd $"($cmd) mode show ($m.id)" -v 1
fmt-info $m.trigger
print ""
}
}
fmt-cmd $"($cmd) mode list" "list all modes"
fmt-cmd $"($cmd) mode show <id> --fmt json|md|yaml|toml|table" "output format"
fmt-cmd $"($cmd) mode select" "interactive selector"
print ""
fmt-section "Execute (requires authorization via .ontoref/config.ncl → mode_run)"
print ""
fmt-cmd $"($cmd) mode run <id>" "execute mode steps with confirmation" -v 1
fmt-cmd $"($cmd) mode run <id> --dry-run" "show steps without executing" -v 1
fmt-cmd $"($cmd) mode run <id> --yes" "skip per-step confirmation" -v 1
print ""
fmt-info "Authorization rules: .ontoref/config.ncl → mode_run.rules"
fmt-info "Each rule matches on { profile, actor, mode_id } — first match wins."
fmt-info "Steps with actor=Human skip for agent/ci, and vice versa."
print ""
fmt-aliases [
{ short: "md", long: "mode" },
{ short: "md l", long: "mode list" },
{ short: "md s", long: "mode show <id>" },
{ short: "md run", long: "mode run <id>" },
]
},
"adr" | "adrs" => {
let brief = adrs-brief
let adr_status = $"($brief.accepted) Accepted / ($brief.superseded) Superseded / ($brief.proposed) Proposed"
print ""
fmt-header $"ADRs (fmt-badge $adr_status)"
fmt-sep
fmt-cmd $"($cmd) adr list" "list all ADRs with status" -v 1
fmt-cmd $"($cmd) adr list --fmt <fmt>" "fmt: table* | md | json | yaml | toml" -v 1
fmt-cmd $"($cmd) adr validate" "run Hard constraint checks" -v 1
fmt-cmd $"($cmd) adr accept <id>" "Proposed → Accepted (patches file in-place)" -v 1
fmt-cmd $"($cmd) adr show" "interactive ADR browser" -v 1
fmt-cmd $"($cmd) adr show -i" "interactive ADR browser [explicit]" -v 1
fmt-cmd $"($cmd) adr show <id>" "show a specific ADR" -v 1
fmt-cmd $"($cmd) adr show <id> --fmt <fmt>" "fmt: md* | table | json | yaml | toml" -v 1
print ""
fmt-section "CONSTRAINTS"
print ""
fmt-cmd $"($cmd) constraint" "show active Hard constraints"
fmt-cmd $"($cmd) constraint --fmt <fmt>" "fmt: table* | md | json | yaml | toml"
print ""
fmt-aliases [
{ short: "ad", long: "adr" },
{ short: "ad l", long: "adr list" },
{ short: "ad a", long: "adr accept <id>" },
{ short: "ad s", long: "adr show [id]" },
]
},
"register" => {
print ""
fmt-header "REGISTER"
fmt-sep
fmt-cmd $"($cmd) register" "record change → CHANGELOG + ADR + ontology + mode"
fmt-cmd $"($cmd) register --backend web" "same, with browser UI"
fmt-cmd $"($cmd) register --backend tui" "same, with terminal 3-panel UI"
print ""
fmt-info "Workflow: select change type → fill form → generates:"
fmt-info " CHANGELOG entry, ADR update, ontology patch, mode trigger"
print ""
fmt-aliases [
{ short: "rg", long: "register" },
]
},
"backlog" => {
print ""
fmt-header "BACKLOG"
fmt-sep
fmt-cmd $"($cmd) backlog roadmap" "roadmap: state dimensions + open items by priority" -v 1
fmt-cmd $"($cmd) backlog list" "open backlog items" -v 1
fmt-cmd $"($cmd) backlog list --status <s>" "filter: Open | InProgress | Done | Cancelled" -v 1
fmt-cmd $"($cmd) backlog list --kind <k>" "filter: Todo | Wish | Idea | Bug | Debt" -v 1
fmt-cmd $"($cmd) backlog list --fmt <fmt>" "fmt: table* | md | json | yaml | toml" -v 1
fmt-cmd $"($cmd) backlog show <id>" "show full item" -v 1
fmt-cmd $"($cmd) backlog add <title>" "create new item" -v 1
fmt-cmd $"($cmd) backlog add <title> --kind <k> --priority <p> --detail <d>" -v 1
fmt-cmd $"($cmd) backlog done <id>" "mark item Done" -v 1
fmt-cmd $"($cmd) backlog cancel <id>" "mark item Cancelled" -v 1
fmt-cmd $"($cmd) backlog promote <id>" "show graduation path → ADR | mode | state" -v 1
print ""
fmt-aliases [
{ short: "bkl", long: "backlog" },
{ short: "bkl r", long: "backlog roadmap" },
{ short: "bkl l", long: "backlog list" },
{ short: "bkl p", long: "backlog promote <id>" },
]
},
"config" => {
print ""
fmt-header "CONFIG PROFILES (sealed immutable states)"
fmt-sep
fmt-section "Read"
print ""
fmt-cmd $"($cmd) config show <profile>" "display active values + current seal" -v 1
fmt-cmd $"($cmd) config show <profile> --fmt json" -v 1
fmt-cmd $"($cmd) config history <profile>" "list sealed cfg-NNN entries" -v 1
fmt-cmd $"($cmd) config diff <profile> <from> <to>" "diff values_snapshot between two seals" -v 1
print ""
fmt-section "Verify"
print ""
fmt-cmd $"($cmd) config verify <profile>" "sha256 drift check (current vs seal)" -v 1
fmt-cmd $"($cmd) config audit" "verify all profiles in manifest" -v 1
print ""
fmt-section "Write"
print ""
fmt-cmd $"($cmd) config apply <profile>" "seal current values → cfg-NNN history entry" -v 1
fmt-cmd $"($cmd) config apply <profile> --adr <id> --pr <id> --bug <id> --note <msg>" -v 1
fmt-cmd $"($cmd) config rollback <profile> <cfg-id>" "restore values (hash-verified)" -v 1
print ""
fmt-aliases [
{ short: "cfg", long: "config" },
]
},
"sync" => {
print ""
fmt-header "SYNC (ontology↔code synchronization)"
fmt-sep
fmt-cmd $"($cmd) sync scan" "analyze project structure" -v 1
fmt-cmd $"($cmd) sync diff" "compare scan against .ontology/core.ncl" -v 1
fmt-cmd $"($cmd) sync diff --quick" "fast diff (skip expensive exports)" -v 1
fmt-cmd $"($cmd) sync propose" "generate NCL patches for drift" -v 1
fmt-cmd $"($cmd) sync apply" "apply changes with confirmation" -v 1
fmt-cmd $"($cmd) sync state" "compare state.ncl dimensions vs reality" -v 1
fmt-cmd $"($cmd) sync audit" "full audit: nodes + ADRs + gates + state" -v 1
fmt-cmd $"($cmd) sync audit --strict" "exit 1 on MISSING/STALE/BROKEN (for CI)" -v 1
fmt-cmd $"($cmd) sync audit --quick" "fast audit (skip API surface)" -v 1
fmt-cmd $"($cmd) sync audit --fmt json" "structured output (for agents)" -v 1
fmt-cmd $"($cmd) sync watch" "bacon-based continuous drift detection" -v 1
print ""
},
"coder" => {
print ""
fmt-header "CODER (.coder/ process memory management)"
fmt-sep
fmt-cmd $"($cmd) coder init <author>" "initialize author workspace" -v 1
fmt-cmd $"($cmd) coder init <author> --actor <a> --model <m>" -v 1
print ""
fmt-section "Record structured JSON entries:"
print ""
fmt-cmd $"($cmd) coder record <author> <content> --title <t> --kind <k> --category <c>" -v 1
fmt-cmd $"($cmd) coder record <author> <content> --tags [t1,t2] --relates_to [n1] --trigger <why>" -v 1
print ""
fmt-section "Query:"
print ""
fmt-cmd $"($cmd) coder log" "all entries across authors" -v 1
fmt-cmd $"($cmd) coder log --author <a>" "filter by author" -v 1
fmt-cmd $"($cmd) coder log --tag <t> --domain <d> --kind <k>" -v 1
fmt-cmd $"($cmd) coder export --format json|jsonl|csv" -v 1
print ""
fmt-section "Markdown triage:"
print ""
fmt-cmd $"($cmd) coder triage <author>" "classify inbox/ files → categories" -v 1
fmt-cmd $"($cmd) coder triage <author> -n" "dry-run" -v 1
fmt-cmd $"($cmd) coder triage <author> -i" "interactive" -v 1
print ""
fmt-section "Lifecycle:"
print ""
fmt-cmd $"($cmd) coder ls <author>" "list files by category" -v 1
fmt-cmd $"($cmd) coder search <pattern>" "search across all .coder/" -v 1
fmt-cmd $"($cmd) coder publish <author> <cat>" "promote to general/" -v 1
fmt-cmd $"($cmd) coder graduate <source_cat>" "copy to committed path" -v 1
fmt-cmd $"($cmd) coder authors" "list all author workspaces" -v 1
print ""
fmt-aliases [
{ short: "cod", long: "coder" },
]
},
"manifest" => {
print ""
fmt-header "MANIFEST (operational modes + publication services)"
fmt-sep
fmt-info "Reads .ontology/manifest.ncl from the project."
print ""
fmt-section "Modes:"
print ""
fmt-cmd $"($cmd) manifest mode <id>" "switch mode (runs pre/post activate)" -v 1
fmt-cmd $"($cmd) manifest mode <id> -n" "dry-run" -v 1
fmt-cmd $"($cmd) manifest mode list" "list available modes" -v 1
print ""
fmt-section "Publish:"
print ""
fmt-cmd $"($cmd) manifest publish <id>" "run publication workflow" -v 1
fmt-cmd $"($cmd) manifest publish <id> -n" "dry-run" -v 1
fmt-cmd $"($cmd) manifest publish <id> -y" "skip confirmation" -v 1
fmt-cmd $"($cmd) manifest publish list" "list publication services" -v 1
print ""
fmt-section "Info:"
print ""
fmt-cmd $"($cmd) manifest layers" "show all layers" -v 1
fmt-cmd $"($cmd) manifest layers --mode <id>" "show visibility for a mode" -v 1
fmt-cmd $"($cmd) manifest consumers" "show consumption modes" -v 1
print ""
fmt-aliases [
{ short: "mf", long: "manifest" },
]
},
"describe" => {
print ""
fmt-header "DESCRIBE (project self-knowledge)"
fmt-sep
fmt-info "Query the project from any perspective. Aggregates ontology,"
fmt-info "ADRs, modes, manifest, justfiles, .claude, and CI config."
print ""
fmt-section "Search the ontology graph"
print ""
fmt-cmd $"($cmd) find <term>" "search + interactive selector with detail" -v 1
fmt-cmd $"($cmd) find <term> --level Project" "filter by level" -v 1
fmt-cmd $"($cmd) find <term> --fmt <fmt>" "fmt: text* | json | yaml | toml | md (short: j y t m)" -v 1
fmt-info "1 result → show detail directly. N results → pick, explore, jump, repeat."
fmt-info "Detail includes: description, artifacts, connections, usage examples."
print ""
fmt-section "What IS this project?"
print ""
fmt-cmd $"($cmd) describe project" "philosophy, axioms, state, gates" -v 1
fmt-cmd $"($cmd) describe project --actor agent" "optimized for agent consumption" -v 1
fmt-cmd $"($cmd) describe project --fmt json" "machine-readable" -v 1
print ""
fmt-section "What can I DO?"
print ""
fmt-cmd $"($cmd) describe capabilities" "just modules, modes, commands, .claude" -v 1
fmt-cmd $"($cmd) describe capabilities --actor ci" "filtered for CI perspective" -v 1
print ""
fmt-section "What can I NOT do?"
print ""
fmt-cmd $"($cmd) describe constraints" "invariants, Hard constraints, gates" -v 1
print ""
fmt-section "What tools are available?"
print ""
fmt-cmd $"($cmd) describe tools" "dev tools, CI tools, just recipes" -v 1
fmt-cmd $"($cmd) describe tools --actor ci" "just CI-relevant recipes" -v 1
print ""
fmt-section "What does this project DO?"
print ""
fmt-cmd $"($cmd) describe features" "list all ontology features + Cargo features" -v 1
fmt-cmd $"($cmd) describe features <id>" "detail: artifacts, deps, edges, dimensions" -v 1
print ""
fmt-section "What happens if I change X?"
print ""
fmt-cmd $"($cmd) describe impact <node-id>" "trace graph edges, show affected nodes" -v 1
fmt-cmd $"($cmd) describe impact <node-id> --depth 3" -v 1
print ""
fmt-section "Why does X exist?"
print ""
fmt-cmd $"($cmd) describe why <id>" "ontology node + ADR + edges" -v 1
print ""
fmt-aliases [
{ short: "d", long: "describe" },
{ short: "d fi", long: "describe find <term>" },
{ short: "d p", long: "describe project" },
{ short: "d cap", long: "describe capabilities" },
{ short: "d con", long: "describe constraints" },
{ short: "d t", long: "describe tools" },
{ short: "d tls", long: "describe tools" },
{ short: "d fea", long: "describe features" },
{ short: "d f", long: "describe features" },
{ short: "d i", long: "describe impact <id>" },
{ short: "d imp", long: "describe impact <id>" },
{ short: "d w", long: "describe why <id>" },
]
},
"log" => {
print ""
fmt-header "LOG (action audit trail)"
fmt-sep
fmt-info "JSONL log of all onref commands. Config in .ontoref/config.ncl → log."
print ""
fmt-section "Query"
print ""
fmt-cmd $"($cmd) log" "show all entries" -v 1
fmt-cmd $"($cmd) log --tail <n>" "last N entries" -v 1
fmt-cmd $"($cmd) log --latest" "most recent entry" -v 1
fmt-cmd $"($cmd) log --since <ts>" "entries after ISO timestamp" -v 1
fmt-cmd $"($cmd) log --until <ts>" "entries before ISO timestamp" -v 1
fmt-cmd $"($cmd) log --level <lvl>" "filter: write | read | interactive" -v 1
fmt-cmd $"($cmd) log --actor <a>" "filter: developer | agent | ci" -v 1
fmt-cmd $"($cmd) log --timestamps" "show timestamps in output" -v 1
fmt-cmd $"($cmd) log --fmt <fmt>" "fmt: text* | json | jsonl | table" -v 1
print ""
fmt-section "Query filter (-q column:text, multiple AND-combined)"
print ""
fmt-cmd $"($cmd) log -q action:describe" "entries where action contains 'describe'" -v 1
fmt-cmd $"($cmd) log -q action:backlog -q level:write" "backlog mutations only" -v 1
fmt-cmd $"($cmd) log -q actor:ci --tail 20 -t" "last 20 CI actions with timestamps" -v 1
print ""
fmt-section "Follow"
print ""
fmt-cmd $"($cmd) log --follow" "tail -f: live output, Ctrl+C to stop" -v 1
fmt-cmd $"($cmd) log -f -q action:sync" "follow only sync commands" -v 1
fmt-cmd $"($cmd) log -f -t" "follow with timestamps" -v 1
print ""
fmt-section "Record (for agents, hooks, scripts)"
print ""
fmt-cmd $"($cmd) log record <action>" "log an action manually" -v 1
fmt-cmd $"($cmd) log record <action> --level write" "explicit level" -v 1
fmt-cmd $"($cmd) log record <action> --author claude --actor agent" "override author/actor" -v 1
print ""
fmt-section "Config"
print ""
fmt-cmd $"($cmd) log config" "show log config, levels, file stats" -v 1
print ""
fmt-section "Columns: ts | author | actor | level | action"
print ""
},
_ => {
print $" (ansi red)Unknown group: ($group)(ansi reset)"
print ""
fmt-info "Available groups: check | form | mode | adr | register | backlog | config | sync | coder | manifest | describe | log"
print ""
},
}
}

View File

@ -0,0 +1,218 @@
# reflection/nulib/interactive.nu — Interactive command picker and group routing.
# Provides the "?" selector, missing-target handler, and run-group-command dispatch.
use ../modules/describe.nu *
use ../modules/config.nu *
use ../modules/sync.nu *
use ../modules/coder.nu *
use ../modules/manifest.nu *
use ../modules/backlog.nu *
use ../modules/adr.nu *
use ./help.nu [help-group]
export def group-command-info [group: string]: nothing -> list<record> {
match $group {
"describe" => [
{ name: project, desc: "philosophy, axioms, state, gates", args: [] },
{ name: capabilities, desc: "just modules, modes, commands, .claude", args: [] },
{ name: constraints, desc: "invariants, Hard constraints, gates", args: [] },
{ name: tools, desc: "dev tools, CI tools, just recipes", args: [] },
{ name: features, desc: "project features list or detail", args: [{name: "id", prompt: "feature id (empty=list all)", optional: true}] },
{ name: impact, desc: "if I change X, what's affected?", args: [{name: "node_id", prompt: "node id", optional: false}] },
{ name: why, desc: "why does X exist?", args: [{name: "id", prompt: "node/adr id", optional: false}] },
],
"backlog" => [
{ name: roadmap, desc: "state dimensions + open items by priority", args: [] },
{ name: list, desc: "open backlog items", args: [] },
{ name: show, desc: "show full backlog item", args: [{name: "id", prompt: "item id", optional: false}] },
{ name: add, desc: "create new backlog item", args: [{name: "title", prompt: "title", optional: false}] },
{ name: done, desc: "mark item as done", args: [{name: "id", prompt: "item id", optional: false}] },
{ name: cancel, desc: "cancel item", args: [{name: "id", prompt: "item id", optional: false}] },
{ name: promote, desc: "promote item priority", args: [{name: "id", prompt: "item id", optional: false}] },
],
"config" => [
{ name: show, desc: "display config profile", args: [{name: "profile", prompt: "profile name", optional: false}] },
{ name: history, desc: "config change history", args: [{name: "profile", prompt: "profile name", optional: false}] },
{ name: diff, desc: "diff between profiles or versions", args: [{name: "profile", prompt: "profile name", optional: false}, {name: "from_id", prompt: "from id", optional: false}, {name: "to_id", prompt: "to id", optional: false}] },
{ name: verify, desc: "verify profile integrity", args: [{name: "profile", prompt: "profile name", optional: false}] },
{ name: audit, desc: "full config audit", args: [] },
{ name: apply, desc: "apply config profile", args: [{name: "profile", prompt: "profile name", optional: false}] },
{ name: rollback, desc: "rollback to previous config", args: [{name: "profile", prompt: "profile name", optional: false}, {name: "to_id", prompt: "target id", optional: false}] },
],
"sync" => [
{ name: scan, desc: "analyze project artifacts", args: [] },
{ name: diff, desc: "compare scan vs ontology", args: [] },
{ name: propose, desc: "generate NCL patches for drift", args: [] },
{ name: apply, desc: "apply proposed changes with confirmation", args: [] },
{ name: state, desc: "sync state.ncl dimensions", args: [] },
{ name: audit, desc: "full ontology↔code audit", args: [] },
{ name: watch, desc: "continuous drift monitoring", args: [] },
],
"coder" => [
{ name: authors, desc: "list registered authors", args: [] },
{ name: init, desc: "initialize author workspace", args: [{name: "author", prompt: "author name", optional: false}] },
{ name: record, desc: "record structured entry", args: [{name: "author", prompt: "author name", optional: false}, {name: "title", prompt: "title", optional: false}, {name: "content", prompt: "content", optional: false}] },
{ name: log, desc: "query recorded entries", args: [] },
{ name: export, desc: "export entries (json/jsonl/csv)", args: [] },
{ name: triage, desc: "classify inbox markdown", args: [{name: "author", prompt: "author name", optional: false}] },
{ name: ls, desc: "list files in author workspace", args: [{name: "author", prompt: "author name (empty=all)", optional: true}] },
{ name: search, desc: "search across entries", args: [{name: "pattern", prompt: "search pattern", optional: false}] },
{ name: publish, desc: "promote entries to general/", args: [{name: "author", prompt: "author name", optional: false}, {name: "category", prompt: "category", optional: false}] },
{ name: graduate, desc: "copy to committed knowledge path", args: [{name: "source", prompt: "source category", optional: false}] },
],
"manifest" => [
{ name: mode, desc: "activate operational mode", args: [{name: "id", prompt: "mode id", optional: false}] },
{ name: publish, desc: "publish artifact to registry", args: [{name: "id", prompt: "service id", optional: false}] },
{ name: layers, desc: "show manifest layers", args: [] },
{ name: consumers, desc: "show consumption modes", args: [] },
],
"adr" => [
{ name: list, desc: "list all ADRs with status", args: [] },
{ name: validate, desc: "run Hard constraint checks", args: [] },
{ name: accept, desc: "Proposed → Accepted", args: [{name: "id", prompt: "adr id (e.g. adr-001)", optional: false}] },
{ name: show, desc: "show ADR detail", args: [{name: "id", prompt: "adr id (empty=interactive)", optional: true}] },
],
_ => [],
}
}
export def group-subcommands [group: string]: nothing -> list<string> {
group-command-info $group | get name | each { |n| $n | into string }
}
export def run-interactive [group: string] {
let cmds = (group-command-info $group)
if ($cmds | is-empty) {
help-group $group
return
}
let labels = ($cmds | each { |c|
let n = ($c.name | into string)
let pad_width = ([(16 - ($n | str length)), 0] | math max)
let pad = (" " | fill -w $pad_width -c ' ')
$"($n)($pad)($c.desc)"
})
let picked_label = ($labels | input list $"(ansi cyan_bold)($group):(ansi reset) ")
if ($picked_label | is-empty) { return }
let idx = ($labels | enumerate | where { |e| $e.item == $picked_label } | first | get index)
let cmd_info = ($cmds | get $idx)
let sub = ($cmd_info.name | into string)
print $" (ansi dark_gray)($cmd_info.desc)(ansi reset)"
mut collected_args: list<string> = []
for arg in $cmd_info.args {
let val = (input $" (ansi cyan)($arg.prompt):(ansi reset) ")
if ($val | is-empty) and (not $arg.optional) {
print $" (ansi yellow)required(ansi reset)"
return
}
$collected_args = ($collected_args | append $val)
}
print ""
let result = (run-group-command $group $sub $collected_args)
if ($result | describe) != "nothing" { print $result }
}
export def run-group-command [group: string, sub: string, args: list<string>] {
let a0 = ($args | get -o 0 | default "")
let a1 = ($args | get -o 1 | default "")
let a2 = ($args | get -o 2 | default "")
match $group {
"describe" => {
match $sub {
"project" => { describe project },
"capabilities" => { describe capabilities },
"constraints" => { describe constraints },
"tools" => { describe tools },
"features" => { if ($a0 | is-empty) { describe features } else { describe features $a0 } },
"impact" => { describe impact $a0 },
"why" => { describe why $a0 },
_ => {},
}
},
"backlog" => {
match $sub {
"roadmap" => { backlog roadmap },
"list" => { backlog list },
"show" => { backlog show $a0 },
"add" => { backlog add $a0 },
"done" => { backlog done $a0 },
"cancel" => { backlog cancel $a0 },
"promote" => { backlog promote $a0 },
_ => {},
}
},
"config" => {
match $sub {
"show" => { config show $a0 },
"history" => { config history $a0 },
"diff" => { config diff $a0 $a1 $a2 },
"verify" => { config verify $a0 },
"audit" => { config audit },
"apply" => { config apply $a0 },
"rollback" => { config rollback $a0 $a1 },
_ => {},
}
},
"sync" => {
match $sub {
"scan" => { sync scan },
"diff" => { sync diff },
"propose" => { sync propose },
"apply" => { sync apply },
"state" => { sync state },
"audit" => { sync audit },
"watch" => { sync watch },
_ => {},
}
},
"coder" => {
match $sub {
"authors" => { coder authors },
"init" => { coder init $a0 },
"record" => { coder record $a0 --title $a1 $a2 },
"log" => { coder log },
"export" => { coder export },
"triage" => { coder triage $a0 },
"ls" => { if ($a0 | is-empty) { coder ls } else { coder ls $a0 } },
"search" => { coder search $a0 },
"publish" => { coder publish $a0 $a1 },
"graduate" => { coder graduate $a0 },
_ => {},
}
},
"manifest" => {
match $sub {
"mode" => { manifest mode $a0 },
"publish" => { manifest publish $a0 },
"layers" => { manifest layers },
"consumers" => { manifest consumers },
_ => {},
}
},
"adr" => {
match $sub {
"list" => { adr list },
"validate" => { adr validate },
"accept" => { adr accept $a0 },
"show" => { if ($a0 | is-empty) { adr show --interactive } else { adr show $a0 } },
_ => {},
}
},
_ => {},
}
}
export def missing-target [group: string, action?: string] {
let act = ($action | default "")
if $act == "h" or $act == "help" {
help-group $group
return
}
if $act == "?" or $act == "select" or $act == "" {
run-interactive $group
return
}
let cmd = ($env.ONTOREF_CALLER? | default "./onref")
print $" (ansi yellow)($group)(ansi reset): unknown subcommand '($act)'. Run '(ansi green)($cmd) ($group) h(ansi reset)' for options."
}

391
reflection/nulib/logger.nu Normal file
View File

@ -0,0 +1,391 @@
# reflection/nulib/logger.nu — JSONL action logger with config-driven rotation.
# Config source: .ontoref/config.ncl → log { level, path, rotation, compress, archive, max_files }
# Env override: ONTOREF_LOG_LEVEL overrides config level if set (for CI/scripts).
use ./shared.nu [project-root]
use ../modules/store.nu [daemon-export-safe]
def level-rank [level: string]: nothing -> int {
match $level {
"write" => 0,
"read" => 1,
"interactive" => 2,
_ => 1,
}
}
# Load log config from .ontoref/config.ncl, with defaults for missing/broken config.
def load-log-config [root: string]: nothing -> record {
let defaults = {
level: "none",
path: ".coder/actions.jsonl",
rotation: "none",
compress: false,
archive: ".coder/archive",
max_files: 10,
}
let config_file = $"($root)/.ontoref/config.ncl"
if not ($config_file | path exists) { return $defaults }
let cfg = (daemon-export-safe $config_file)
if $cfg == null { return $defaults }
let log = ($cfg.log? | default {})
{
level: ($log.level? | default $defaults.level),
path: ($log.path? | default $defaults.path),
rotation: ($log.rotation? | default $defaults.rotation),
compress: ($log.compress? | default $defaults.compress),
archive: ($log.archive? | default $defaults.archive),
max_files: ($log.max_files? | default $defaults.max_files),
}
}
# Determine rotation suffix from current date and policy.
def rotation-suffix [policy: string, ts: datetime]: nothing -> string {
match $policy {
"daily" => ($ts | format date "%Y-%m-%d"),
"weekly" => ($ts | format date "%Y-W%V"),
"monthly" => ($ts | format date "%Y-%m"),
_ => "",
}
}
# Check if the current log file needs rotation. Returns true if rotation boundary crossed.
def needs-rotation [log_file: string, policy: string]: nothing -> bool {
if $policy == "none" { return false }
if not ($log_file | path exists) { return false }
let stat = ls -l $log_file | first
let file_modified = $stat.modified
let now = (date now)
let file_suffix = (rotation-suffix $policy $file_modified)
let current_suffix = (rotation-suffix $policy $now)
$file_suffix != $current_suffix
}
# Rotate: move current log to archive with date suffix, optionally compress, prune old files.
def rotate-log [root: string, cfg: record] {
let log_file = $"($root)/($cfg.path)"
if not ($log_file | path exists) { return }
let archive_dir = $"($root)/($cfg.archive)"
if not ($archive_dir | path exists) { mkdir $archive_dir }
let basename = ($log_file | path parse | get stem)
let stat = ls -l $log_file | first
let suffix = (rotation-suffix $cfg.rotation $stat.modified)
let archive_name = $"($basename)-($suffix).jsonl"
let archive_path = $"($archive_dir)/($archive_name)"
mv $log_file $archive_path
if $cfg.compress {
let gz_result = do { ^gzip $archive_path } | complete
if $gz_result.exit_code != 0 {
let err_msg = ($gz_result.stderr | str trim)
print -e $" warn: gzip failed for ($archive_path): ($err_msg)"
}
}
# Prune: keep only max_files most recent archives
let pattern = $"($archive_dir)/($basename)-*"
let archives = (glob $pattern | sort -r)
if ($archives | length) > $cfg.max_files {
let to_remove = ($archives | skip $cfg.max_files)
for f in $to_remove { rm $f }
}
}
# ── Query commands (exported for dispatcher) ────────────────────────────────
# Show log config: path, resolved file, settings.
export def log-show-config [] {
let root = (project-root)
let cfg = (load-log-config $root)
let log_file = $"($root)/($cfg.path)"
let archive_dir = $"($root)/($cfg.archive)"
let file_exists = ($log_file | path exists)
let file_size = if $file_exists { (ls $log_file | first | get size) } else { 0 }
let line_count = if $file_exists { (open $log_file --raw | lines | where { $in | is-not-empty } | length) } else { 0 }
let archive_count = if ($archive_dir | path exists) {
let basename = ($log_file | path parse | get stem)
glob $"($archive_dir)/($basename)-*" | length
} else { 0 }
print ""
print $" (ansi white_bold)Log config(ansi reset) (ansi dark_gray)\(.ontoref/config.ncl → log\)(ansi reset)"
print ""
print $" (ansi cyan)level(ansi reset) ($cfg.level)"
print $" (ansi cyan)path(ansi reset) ($cfg.path)"
print $" (ansi cyan)rotation(ansi reset) ($cfg.rotation)"
print $" (ansi cyan)compress(ansi reset) ($cfg.compress)"
print $" (ansi cyan)archive(ansi reset) ($cfg.archive)"
print $" (ansi cyan)max_files(ansi reset) ($cfg.max_files)"
print ""
print $" (ansi white_bold)Levels(ansi reset) (ansi dark_gray)\(cumulative — each includes all below\)(ansi reset)"
print ""
let levels = [
["level" "logs" "rank"];
["none" "nothing" "—"]
["write" "mutations only (add, done, apply, accept...)" "0"]
["read" "mutations + queries (list, show, status, describe...)" "0+1"]
["all" "mutations + queries + interactive (help, selectors)" "0+1+2"]
]
let active_level = ($env.ONTOREF_LOG_LEVEL? | default $cfg.level)
for row in $levels {
let marker = if $row.level == $active_level { $"(ansi green_bold)●(ansi reset)" } else { $"(ansi dark_gray)○(ansi reset)" }
print $" ($marker) (ansi cyan)($row.level | fill -w 6)(ansi reset) ($row.logs)"
}
if ($env.ONTOREF_LOG_LEVEL? | is-not-empty) {
print ""
print $" (ansi dark_gray)env override: ONTOREF_LOG_LEVEL=($env.ONTOREF_LOG_LEVEL)(ansi reset)"
}
print ""
print $" (ansi white_bold)Current file(ansi reset)"
print $" (ansi cyan)path(ansi reset) ($log_file)"
print $" (ansi cyan)exists(ansi reset) ($file_exists)"
print $" (ansi cyan)size(ansi reset) ($file_size)"
print $" (ansi cyan)entries(ansi reset) ($line_count)"
print $" (ansi cyan)archived(ansi reset) ($archive_count) files"
print ""
print $" (ansi white_bold)Columns(ansi reset) ts | author | actor | level | action"
print ""
}
# Parse JSONL log entries from the active log file.
# Guards against malformed lines: from json is an internal command and cannot
# be captured with | complete, so we pre-filter to lines that look like objects.
def load-entries [root: string, cfg: record]: nothing -> list<record> {
let log_file = $"($root)/($cfg.path)"
if not ($log_file | path exists) { return [] }
open $log_file --raw
| lines
| where { $in | str trim | is-not-empty }
| where { ($in | str trim | str starts-with "{") and ($in | str trim | str ends-with "}") }
| each { |line| try { $line | str trim | from json } catch { null } }
| where { $in != null }
}
# Filter entries by --since/--until (ISO 8601 string comparison).
def filter-time-range [entries: list<record>, since: string, until: string]: nothing -> list<record> {
mut result = $entries
if ($since | is-not-empty) {
$result = ($result | where { |e| ($e.ts? | default "") >= $since })
}
if ($until | is-not-empty) {
$result = ($result | where { |e| ($e.ts? | default "") <= $until })
}
$result
}
# Apply column:match-text query filters. Multiple queries are AND-combined.
# Each query is "column:text" — matches if the column value contains text.
def filter-query [entries: list<record>, queries: list<string>]: nothing -> list<record> {
mut result = $entries
for q in $queries {
let parts = ($q | split row ":" | collect)
if ($parts | length) < 2 { continue }
let col = ($parts | first)
let pattern = ($parts | skip 1 | str join ":")
$result = ($result | where { |e|
let val = ($e | get -o $col | default "")
($val | str contains $pattern)
})
}
$result
}
# Format a single entry for terminal display.
def fmt-entry [entry: record, timestamps: bool]: nothing -> string {
let level_color = match ($entry.level? | default "read") {
"write" => (ansi yellow),
"read" => (ansi cyan),
"interactive" => (ansi magenta),
_ => (ansi default_dimmed),
}
let actor_str = $"(ansi dark_gray)($entry.actor? | default '?')(ansi reset)"
let action_str = $"($entry.action? | default '')"
if $timestamps {
let ts_str = $"(ansi dark_gray)($entry.ts? | default '')(ansi reset)"
$" ($ts_str) ($level_color)($entry.level? | default '?')(ansi reset) ($actor_str) ($action_str)"
} else {
$" ($level_color)($entry.level? | default '?')(ansi reset) ($actor_str) ($action_str)"
}
}
# Query log entries with filters. Used by `strat log`.
export def log-query [
--tail_n: int = -1,
--since: string = "",
--until: string = "",
--latest,
--timestamps (-t),
--level: string = "",
--actor: string = "",
--query (-q): list<string> = [],
--fmt (-f): string = "text",
] {
let root = (project-root)
let cfg = (load-log-config $root)
let log_file = $"($root)/($cfg.path)"
if not ($log_file | path exists) {
print " No log entries."
return
}
mut entries = (load-entries $root $cfg)
$entries = (filter-time-range $entries $since $until)
if ($level | is-not-empty) {
$entries = ($entries | where { |e| ($e.level? | default "") == $level })
}
if ($actor | is-not-empty) {
$entries = ($entries | where { |e| ($e.actor? | default "") == $actor })
}
if ($query | is-not-empty) {
$entries = (filter-query $entries $query)
}
if $latest {
$entries = if ($entries | is-empty) { [] } else { [($entries | last)] }
} else if $tail_n > 0 {
let total = ($entries | length)
if $total > $tail_n {
$entries = ($entries | skip ($total - $tail_n))
}
}
match $fmt {
"json" => { print ($entries | to json) },
"jsonl" => {
for e in $entries { print ($e | to json -r) }
},
"table" => { print ($entries | table --expand) },
_ => {
if ($entries | is-empty) {
print " No log entries."
return
}
for e in $entries {
print (fmt-entry $e $timestamps)
}
},
}
}
# Follow log output — polls for new entries every 2 seconds.
export def log-follow [--timestamps (-t), --query (-q): list<string> = []] {
let root = (project-root)
let cfg = (load-log-config $root)
let log_file = $"($root)/($cfg.path)"
let has_filter = ($query | is-not-empty)
let filter_label = if $has_filter { $" (ansi cyan)filter: ($query | str join ', ')(ansi reset)" } else { "" }
print $" (ansi dark_gray)Following ($cfg.path)(ansi reset)($filter_label)(ansi dark_gray) — Ctrl+C to stop(ansi reset)"
print ""
mut last_count = if ($log_file | path exists) {
(load-entries $root $cfg) | length
} else { 0 }
# Show last 10 entries as context (filtered if query given)
if $last_count > 0 {
mut entries = (load-entries $root $cfg)
if $has_filter { $entries = (filter-query $entries $query) }
let total = ($entries | length)
let start = if $total > 10 { $total - 10 } else { 0 }
let tail_entries = ($entries | skip $start)
for e in $tail_entries {
print (fmt-entry $e $timestamps)
}
}
loop {
sleep 2sec
let current_count = if ($log_file | path exists) {
(load-entries $root $cfg) | length
} else { 0 }
if $current_count < $last_count {
# File was rotated (replaced with a shorter one) — reset position.
$last_count = 0
}
if $current_count > $last_count {
let entries = (load-entries $root $cfg)
mut new_entries = ($entries | skip $last_count)
if $has_filter { $new_entries = (filter-query $new_entries $query) }
for e in $new_entries {
print (fmt-entry $e $timestamps)
}
$last_count = $current_count
}
}
}
# ── Write ────────────────────────────────────────────────────────────────────
# Manual record — for agents, hooks, scripts, and any external actor.
# Bypasses level filtering (always writes if log is not "none").
export def log-record [
action: string,
--level (-l): string = "write",
--author (-a): string = "",
--actor: string = "",
] {
let root = (project-root)
let cfg = (load-log-config $root)
let effective_level = ($env.ONTOREF_LOG_LEVEL? | default $cfg.level)
if $effective_level == "none" { return }
let log_file = $"($root)/($cfg.path)"
let dir = ($log_file | path dirname)
if not ($dir | path exists) { mkdir $dir }
if (needs-rotation $log_file $cfg.rotation) {
rotate-log $root $cfg
}
let ts = (date now | format date "%Y-%m-%dT%H:%M:%S%z")
let resolved_author = if ($author | is-not-empty) { $author } else { ($env.ONTOREF_AUTHOR? | default "unknown") }
let resolved_actor = if ($actor | is-not-empty) { $actor } else { ($env.ONTOREF_ACTOR? | default "developer") }
let entry = { ts: $ts, author: $resolved_author, actor: $resolved_actor, level: $level, action: $action }
let line = ($entry | to json --raw)
$line + "\n" | save -a $log_file
}
# Auto log — called internally by dispatcher on each canonical command.
export def log-action [action: string, level: string = "read"] {
let root = (project-root)
let cfg = (load-log-config $root)
# Env override: ONTOREF_LOG_LEVEL takes precedence over config
let effective_level = ($env.ONTOREF_LOG_LEVEL? | default $cfg.level)
if $effective_level == "none" { return }
# "all" maps to interactive (rank 2) — allows everything through
let threshold_key = if $effective_level == "all" { "interactive" } else { $effective_level }
let action_rank = (level-rank $level)
let threshold_rank = (level-rank $threshold_key)
if $action_rank > $threshold_rank { return }
let log_file = $"($root)/($cfg.path)"
let dir = ($log_file | path dirname)
if not ($dir | path exists) { mkdir $dir }
# Rotation check before write
if (needs-rotation $log_file $cfg.rotation) {
rotate-log $root $cfg
}
let ts = (date now | format date "%Y-%m-%dT%H:%M:%S%z")
let author = ($env.ONTOREF_AUTHOR? | default "unknown")
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let entry = { ts: $ts, author: $author, actor: $actor, level: $level, action: $action }
let line = ($entry | to json --raw)
$line + "\n" | save -a $log_file
}

367
reflection/nulib/modes.nu Normal file
View File

@ -0,0 +1,367 @@
# reflection/nulib/modes.nu — Mode listing, detail, rendering, and execution.
use ./shared.nu [all-mode-files, project-root]
use ./help.nu [help-group]
use ../modules/store.nu [daemon-export, daemon-export-safe]
# ── List / Show ──────────────────────────────────────────────────────────────
export def list-modes []: nothing -> list<record> {
let mode_files = (all-mode-files)
$mode_files | each { |mf|
let m = (daemon-export-safe $mf)
if $m != null {
{
id: ($m.id? | default ""),
trigger: ($m.trigger? | default ""),
steps: ($m.steps? | default [] | length),
preconditions: ($m.preconditions? | default [] | length),
}
} else { null }
} | compact
}
export def show-mode [id: string, fmt_resolved: string] {
let mode_files = (all-mode-files)
let candidates = ($mode_files | where { |p| ($p | path basename | str replace ".ncl" "") == $id })
if ($candidates | is-empty) {
let available = ($mode_files | each { |p| $p | path basename | str replace ".ncl" "" })
error make { msg: $"Mode '($id)' not found. Available: ($available | str join ', ')" }
}
let m = (daemon-export ($candidates | first))
match $fmt_resolved {
"json" => { $m | to json },
"yaml" => { $m | to yaml },
"toml" => { $m | to toml },
"table" => { $m | table --expand },
_ => { mode-to-md $m },
}
}
def mode-to-md [m: record]: nothing -> string {
mut lines = []
$lines = ($lines | append $"# ($m.id)")
$lines = ($lines | append "")
$lines = ($lines | append $"**Trigger**: ($m.trigger)")
$lines = ($lines | append "")
if ($m.preconditions? | is-not-empty) and (($m.preconditions | length) > 0) {
$lines = ($lines | append "## Preconditions")
for p in $m.preconditions { $lines = ($lines | append $"- ($p)") }
$lines = ($lines | append "")
}
$lines = ($lines | append "## Steps")
for s in $m.steps {
$lines = ($lines | append $"### ($s.id) [($s.actor)]")
$lines = ($lines | append $s.action)
if ($s.cmd? | is-not-empty) { $lines = ($lines | append $"```\n($s.cmd)\n```") }
if ($s.depends_on? | is-not-empty) and (($s.depends_on | length) > 0) {
$lines = ($lines | append $"Depends on: ($s.depends_on | each { |d| $d.step } | str join ', ')")
}
$lines = ($lines | append $"On error: ($s.on_error.strategy)")
$lines = ($lines | append "")
}
if ($m.postconditions? | is-not-empty) and (($m.postconditions | length) > 0) {
$lines = ($lines | append "## Postconditions")
for p in $m.postconditions { $lines = ($lines | append $"- ($p)") }
}
$lines | str join "\n"
}
export def run-modes-interactive [modes: list<record>] {
let ids = ($modes | get id)
let picked = ($ids | input list $"(ansi cyan_bold)Mode:(ansi reset) ")
if ($picked | is-not-empty) {
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let f = if $actor == "agent" { "json" } else { "md" }
show-mode $picked $f
}
}
# ── Authorization ────────────────────────────────────────────────────────────
# Load mode_run config from .ontoref/config.ncl.
def load-mode-run-config [root: string]: nothing -> record {
let defaults = {
rules: [],
default_allow: false,
confirm_each_step: true,
}
let config_file = $"($root)/.ontoref/config.ncl"
if not ($config_file | path exists) { return $defaults }
let cfg = (daemon-export-safe $config_file)
if $cfg == null { return $defaults }
let mr = ($cfg.mode_run? | default {})
{
rules: ($mr.rules? | default $defaults.rules),
default_allow: ($mr.default_allow? | default $defaults.default_allow),
confirm_each_step: ($mr.confirm_each_step? | default $defaults.confirm_each_step),
}
}
# Load profile name from .ontoref/config.ncl.
def load-profile [root: string]: nothing -> string {
let config_file = $"($root)/.ontoref/config.ncl"
if not ($config_file | path exists) { return "unknown" }
let cfg = (daemon-export-safe $config_file)
if $cfg == null { return "unknown" }
$cfg.profile? | default "unknown"
}
# Check if a single rule's `when` clause matches the context.
# All present fields must match (AND). Missing fields = don't care.
def rule-matches [when_clause: record, context: record]: nothing -> bool {
let profile_ok = if ($when_clause.profile? | is-not-empty) {
($when_clause.profile == $context.profile)
} else { true }
let actor_ok = if ($when_clause.actor? | is-not-empty) {
($when_clause.actor == $context.actor)
} else { true }
let mode_ok = if ($when_clause.mode_id? | is-not-empty) {
($when_clause.mode_id == $context.mode_id)
} else { true }
$profile_ok and $actor_ok and $mode_ok
}
# Evaluate authorization rules. Returns { allowed: bool, reason: string }.
def authorize-mode [mode_id: string, root: string]: nothing -> record {
let mr_cfg = (load-mode-run-config $root)
let profile = (load-profile $root)
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let context = { profile: $profile, actor: $actor, mode_id: $mode_id }
# First matching rule wins.
mut result = { allowed: $mr_cfg.default_allow, reason: "no matching rule — default policy" }
for rule in $mr_cfg.rules {
if (rule-matches $rule.when $context) {
let reason = if ($rule.reason? | is-not-empty) { $rule.reason } else { "matched rule" }
$result = { allowed: $rule.allow, reason: $reason }
break
}
}
$result
}
# ── Runner ───────────────────────────────────────────────────────────────────
# Load a mode by id, returning the full NCL-exported record.
def load-mode [id: string]: nothing -> record {
let mode_files = (all-mode-files)
let candidates = ($mode_files | where { |p| ($p | path basename | str replace ".ncl" "") == $id })
if ($candidates | is-empty) {
let available = ($mode_files | each { |p| $p | path basename | str replace ".ncl" "" })
error make { msg: $"Mode '($id)' not found. Available: ($available | str join ', ')" }
}
daemon-export ($candidates | first)
}
# Check if the current actor can execute a step based on the step's actor field.
def actor-can-run-step [step_actor: string]: nothing -> bool {
let current = ($env.ONTOREF_ACTOR? | default "developer")
match $step_actor {
"Both" => true,
"Human" => ($current == "developer" or $current == "admin"),
"Agent" => ($current == "agent" or $current == "ci"),
_ => true,
}
}
# Execute a single step's command. Returns { success: bool, output: string }.
def exec-step-cmd [cmd: string]: nothing -> record {
let result = do { ^bash -c $cmd } | complete
{
success: ($result.exit_code == 0),
output: (if $result.exit_code == 0 { $result.stdout } else { $result.stderr }),
}
}
# Print step summary before execution.
def print-step-header [step: record, index: int, total: int] {
let actor_color = match ($step.actor? | default "Both") {
"Human" => (ansi yellow),
"Agent" => (ansi magenta),
_ => (ansi cyan),
}
print $" (ansi white_bold)\(($index + 1)/($total)\)(ansi reset) (ansi cyan_bold)($step.id)(ansi reset) (ansi dark_gray)[($actor_color)($step.actor? | default 'Both')(ansi reset)(ansi dark_gray)](ansi reset)"
print $" ($step.action? | default '')"
if ($step.cmd? | is-not-empty) {
print $" (ansi dark_gray)$ ($step.cmd)(ansi reset)"
}
}
# Run a mode: authorize → preconditions → steps → postconditions.
export def run-mode [id: string, --dry-run, --yes] {
let root = (project-root)
# ── Load mode ──
let m = (load-mode $id)
# ── Authorization ──
let auth = (authorize-mode $id $root)
if not $auth.allowed {
let profile = (load-profile $root)
let actor = ($env.ONTOREF_ACTOR? | default "developer")
let steps = ($m.steps? | default [])
print ""
print $" (ansi white_bold)($m.id)(ansi reset) (ansi dark_gray)($steps | length) steps(ansi reset)"
print $" ($m.trigger? | default '')"
print ""
# Show steps with their commands so the user knows what to run manually.
if ($steps | is-not-empty) {
print $" (ansi white_bold)Steps \(manual execution\):(ansi reset)"
for step in ($steps | enumerate) {
let s = $step.item
let cmd_display = if ($s.cmd? | is-not-empty) { $"(ansi dark_gray)$ ($s.cmd)(ansi reset)" } else { $"(ansi dark_gray)— informational(ansi reset)" }
print $" (ansi dark_gray)($step.index + 1).(ansi reset) (ansi cyan)($s.id)(ansi reset) [($s.actor? | default 'Both')] ($cmd_display)"
print $" ($s.action? | default '')"
}
print ""
}
print $" (ansi red_bold)DENIED(ansi reset) Automated execution blocked."
print $" (ansi dark_gray)Reason: ($auth.reason)(ansi reset)"
print $" (ansi dark_gray)Context: profile=($profile), actor=($actor)(ansi reset)"
print $" (ansi dark_gray)Configure in .ontoref/config.ncl → mode_run.rules(ansi reset)"
print ""
return
}
let steps = ($m.steps? | default [])
let preconditions = ($m.preconditions? | default [])
let postconditions = ($m.postconditions? | default [])
let mr_cfg = (load-mode-run-config $root)
print ""
print $" (ansi green_bold)AUTHORIZED(ansi reset) ($auth.reason)"
print $" (ansi white_bold)Mode:(ansi reset) ($m.id) (ansi dark_gray)($steps | length) steps(ansi reset)"
print $" ($m.trigger? | default '')"
print ""
# ── Preconditions ──
if ($preconditions | is-not-empty) {
print $" (ansi white_bold)Preconditions(ansi reset)"
for p in $preconditions {
print $" (ansi dark_gray)●(ansi reset) ($p)"
}
print ""
}
if $dry_run {
print $" (ansi yellow_bold)DRY RUN(ansi reset) — showing steps without executing"
print ""
for step in ($steps | enumerate) {
print-step-header $step.item $step.index ($steps | length)
print ""
}
if ($postconditions | is-not-empty) {
print $" (ansi white_bold)Postconditions(ansi reset)"
for p in $postconditions { print $" (ansi dark_gray)●(ansi reset) ($p)" }
}
return
}
# ── Execute steps ──
mut failed_steps = []
for step in ($steps | enumerate) {
let s = $step.item
let idx = $step.index
# Skip steps whose actor doesn't match current actor.
if not (actor-can-run-step ($s.actor? | default "Both")) {
print $" (ansi dark_gray)SKIP(ansi reset) ($s.id) (ansi dark_gray)[requires ($s.actor)](ansi reset)"
continue
}
# Check depends_on: if a dependency failed and its kind is OnSuccess, skip.
let deps = ($s.depends_on? | default [])
mut dep_blocked = false
for dep in $deps {
let dep_kind = ($dep.kind? | default "OnSuccess")
if $dep_kind == "OnSuccess" and ($failed_steps | any { |f| $f == $dep.step }) {
$dep_blocked = true
break
}
}
if $dep_blocked {
print $" (ansi yellow)BLOCKED(ansi reset) ($s.id) (ansi dark_gray)— dependency failed(ansi reset)"
continue
}
print-step-header $s $idx ($steps | length)
# No command = informational step, just display.
if ($s.cmd? | is-empty) or ($s.cmd == "") {
print $" (ansi dark_gray)no command — informational step(ansi reset)"
print ""
continue
}
# Confirm if required and not --yes.
if $mr_cfg.confirm_each_step and (not $yes) {
let answer = (input $" (ansi cyan)Execute? [y/N/q](ansi reset) " | str trim | str downcase)
if $answer == "q" {
print $" (ansi yellow)Aborted by user.(ansi reset)"
return
}
if $answer != "y" {
print $" (ansi dark_gray)skipped(ansi reset)"
print ""
continue
}
}
# Execute.
let result = (exec-step-cmd $s.cmd)
if $result.success {
print $" (ansi green)OK(ansi reset)"
if ($result.output | is-not-empty) {
let trimmed = ($result.output | str trim)
if ($trimmed | is-not-empty) {
$trimmed | lines | each { |l| print $" (ansi dark_gray)│(ansi reset) ($l)" } | ignore
}
}
} else {
let strategy = ($s.on_error? | default {} | get -o strategy | default "Stop")
$failed_steps = ($failed_steps | append $s.id)
if $strategy == "Stop" {
print $" (ansi red_bold)FAILED(ansi reset) (ansi dark_gray)— strategy: Stop(ansi reset)"
if ($result.output | is-not-empty) {
$result.output | str trim | lines | each { |l| print $" (ansi red)│(ansi reset) ($l)" } | ignore
}
print ""
print $" (ansi red)Execution halted at step ($s.id).(ansi reset)"
return
} else {
print $" (ansi yellow)FAILED(ansi reset) (ansi dark_gray)— strategy: Continue(ansi reset)"
if ($result.output | is-not-empty) {
$result.output | str trim | lines | each { |l| print $" (ansi yellow)│(ansi reset) ($l)" } | ignore
}
}
}
print ""
}
# ── Postconditions ──
if ($postconditions | is-not-empty) {
print $" (ansi white_bold)Postconditions(ansi reset)"
for p in $postconditions { print $" (ansi dark_gray)●(ansi reset) ($p)" }
print ""
}
let fail_count = ($failed_steps | length)
if $fail_count == 0 {
print $" (ansi green_bold)COMPLETE(ansi reset) All steps executed successfully."
} else {
print $" (ansi yellow_bold)PARTIAL(ansi reset) ($fail_count) step(s) failed: ($failed_steps | str join ', ')"
}
print ""
}

View File

@ -0,0 +1,82 @@
# reflection/nulib/shared.nu — Shared utilities used across multiple nulib modules.
use ../modules/store.nu [daemon-export-safe]
# Collect mode files from both ONTOREF_ROOT and project-local reflection/modes/.
export def all-mode-files []: nothing -> list<string> {
let root_modes = (glob $"($env.ONTOREF_ROOT)/reflection/modes/*.ncl")
let project_modes = if ($env.ONTOREF_PROJECT_ROOT? | is-not-empty) and ($env.ONTOREF_PROJECT_ROOT? != $env.ONTOREF_ROOT) {
glob $"($env.ONTOREF_PROJECT_ROOT)/reflection/modes/*.ncl"
} else { [] }
$root_modes | append $project_modes | uniq
}
# Quick ADR status counts (accepted/superseded/proposed).
export def adrs-brief []: nothing -> record {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let statuses = (
glob $"($root)/adrs/adr-*.ncl" | each { |f|
let data = (daemon-export-safe $f)
if $data != null { $data.status? | default null } else { null }
} | compact
)
{
accepted: ($statuses | where $it == "Accepted" | length),
superseded: ($statuses | where $it == "Superseded" | length),
proposed: ($statuses | where $it == "Proposed" | length),
}
}
# Resolve project root: ONTOREF_PROJECT_ROOT if set and different, else ONTOREF_ROOT.
export 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 }
}
# Build NICKEL_IMPORT_PATH for a project root.
export def nickel-import-path [root: string]: nothing -> string {
let entries = [
$"($root)/.ontology"
$"($root)/adrs"
$"($root)/.ontoref/ontology/schemas"
$"($root)/.ontoref/adrs"
$"($root)/.onref"
$root
$"($env.ONTOREF_ROOT)/ontology"
$"($env.ONTOREF_ROOT)/ontology/schemas"
$"($env.ONTOREF_ROOT)/adrs"
$env.ONTOREF_ROOT
]
let valid = ($entries | where { |p| $p | path exists } | uniq)
let existing = ($env.NICKEL_IMPORT_PATH? | default "")
if ($existing | is-not-empty) {
($valid | append $existing) | str join ":"
} else {
$valid | str join ":"
}
}
# Read state.ncl dimensions from a project root.
export def load-dimensions [root: string]: nothing -> list<record> {
let state_file = $"($root)/.ontology/state.ncl"
if not ($state_file | path exists) { return [] }
let ip = (nickel-import-path $root)
let state = (daemon-export-safe $state_file --import-path $ip)
if $state == null { return [] }
$state.dimensions? | default [] | each { |d| {
id: $d.id,
current_state: $d.current_state,
desired_state: $d.desired_state,
reached: ($d.current_state == $d.desired_state),
}}
}
# Read active gates from a project root.
export def load-gates [root: string]: nothing -> list<record> {
let gate_file = $"($root)/.ontology/gate.ncl"
if not ($gate_file | path exists) { return [] }
let ip = (nickel-import-path $root)
let gate = (daemon-export-safe $gate_file --import-path $ip)
if $gate == null { return [] }
$gate.membranes? | default [] | where { |m| ($m.active? | default false) == true }
}

5
reflection/qa.ncl Normal file
View File

@ -0,0 +1,5 @@
let s = import "qa" in
{
entries = [],
} | s.QaStore

View File

@ -0,0 +1,179 @@
let s = import "./schema.ncl" in
# Tool and dependency requirements for the ontoref ecosystem.
# Query (agent or newcomer):
# nickel export reflection/requirements/base.ncl | get tools | select id binary version_min install_hint
#
# Query by context:
# nickel export reflection/requirements/base.ncl | get tools | where (contexts | any { |c| $c == "local_dev" })
#
# Validate environment:
# nu reflection/bin/check-prereqs.nu --context local_dev
{
tools = [
# ── Core (required in every context) ─────────────────────────────────────
({
id = "nickel",
binary = "nickel",
description = "Type-safe configuration language. Primary config source of truth (ADR-001).",
version_min = "1.9.0",
check_cmd = "nickel --version",
version_extract = "nickel (\\d+\\.\\d+\\.\\d+)",
install_hint = "cargo install nickel-lang-cli # or: brew install nickel-lang",
docs_url = "https://nickel-lang.org",
severity = 'Hard,
contexts = ["local_dev", "ci", "agent", "benchmark"],
used_by_forms = ["new_adr", "query_constraints", "new_project", "supersede_adr"],
used_by_modes = ["new_adr", "read_as_agent", "validate_decision", "supersede_adr"],
} | s.Tool),
({
id = "nushell",
binary = "nu",
description = "Structured shell and scripting language. Used for check_hints, scripts, and mode execution.",
version_min = "0.110.0",
check_cmd = "nu --version",
version_extract = "(\\d+\\.\\d+\\.\\d+)",
install_hint = "cargo install nu # or: brew install nushell",
docs_url = "https://nushell.sh",
severity = 'Hard,
contexts = ["local_dev", "ci", "agent", "benchmark"],
used_by_forms = ["new_project", "supersede_adr"],
used_by_modes = ["validate_decision", "new_project", "supersede_adr", "query_constraints"],
config_check = "nu -c 'version | get version'",
config_hint = "Minimum 0.110.0 required for pipeline let binding and `| complete` patterns.",
} | s.Tool),
({
id = "git",
binary = "git",
description = "Version control. Required for ADR commit workflow and project initialization.",
version_min = "2.40.0",
check_cmd = "git --version",
version_extract = "git version (\\d+\\.\\d+\\.\\d+)",
install_hint = "brew install git # or: system package manager",
severity = 'Hard,
contexts = ["local_dev", "ci"],
used_by_forms = ["new_adr", "supersede_adr", "new_project"],
used_by_modes = ["new_adr", "supersede_adr", "new_project"],
} | s.Tool),
# ── Interactive / form layer ──────────────────────────────────────────────
({
id = "typedialog",
binary = "typedialog",
description = "Multibackend form runner. Required for interactive ADR authoring and project initialization. Forms are NCL records (not TOML). Uses nickel-roundtrip for editing existing files.",
version_min = "0.1.0",
check_cmd = "typedialog --version",
version_extract = "typedialog (\\d+\\.\\d+\\.\\d+)",
install_hint = "cargo install typedialog",
docs_url = "https://repo.jesusperez.pro/jesus/typedialog",
severity = 'Soft,
contexts = ["local_dev"],
used_by_forms = ["new_adr", "new_project", "supersede_adr", "query_constraints"],
used_by_modes = ["new_adr", "new_project", "supersede_adr"],
config_check = "do { ^typedialog --version } | complete | get stdout | str contains '0.'",
config_hint = "Forms use NCL format (not TOML). Command: typedialog nickel-roundtrip --input <file.ncl> --form <form.ncl> --output <out.ncl>",
} | s.Tool),
# ── NATS ─────────────────────────────────────────────────────────────────
({
id = "nats-cli",
binary = "nats",
description = "NATS CLI for stream creation and message publishing. Required for ecosystem event publishing and NATS stream setup.",
version_min = "0.1.0",
check_cmd = "nats --version",
version_extract = "(\\d+\\.\\d+\\.\\d+)",
install_hint = "brew install nats-io/nats-tools/nats # or: go install github.com/nats-io/natscli/nats@latest",
severity = 'Soft,
contexts = ["local_dev"],
used_by_forms = ["new_project"],
used_by_modes = ["new_project"],
config_check = "do { ^nats server check --server nats://localhost:4222 } | complete | get stdout | str contains 'OK'",
config_hint = "NATS server must be running at nats://localhost:4222 for ecosystem event publishing. Start: nats-server -js",
} | s.Tool),
# ── Nu plugins ───────────────────────────────────────────────────────────
# Checked via `plugin list`, not `which`. plugin_name drives the check path.
({
id = "nu-plugin-nickel",
binary = "nu_plugin_nickel",
plugin_name = "nickel",
description = "Nushell plugin for Nickel. Provides nickel-export (no from json), nickel-eval (cached), nickel-validate, nickel-format.",
version_min = "0.110.0",
check_cmd = "plugin list | where name == 'nickel' | first | get version",
install_hint = "plugin add ~/.local/bin/nu_plugin_nickel # then restart nu",
severity = 'Soft,
contexts = ["local_dev", "agent"],
used_by_forms = ["new_adr", "new_project", "supersede_adr"],
used_by_modes = ["new_adr", "new_project"],
} | s.Tool),
({
id = "nu-plugin-tera",
binary = "nu_plugin_tera",
plugin_name = "tera",
description = "Nushell plugin for Tera (Jinja2-compatible) template rendering. Used by agent render path: nickel-export | tera-render template.ncl.j2.",
version_min = "0.110.0",
check_cmd = "plugin list | where name == 'tera' | first | get version",
install_hint = "plugin add ~/.local/bin/nu_plugin_tera # then restart nu",
severity = 'Soft,
contexts = ["local_dev", "agent"],
used_by_forms = ["new_adr", "new_project", "supersede_adr"],
used_by_modes = ["new_adr", "new_project"],
} | s.Tool),
({
id = "nu-plugin-nats",
binary = "nu_plugin_nats",
plugin_name = "nats",
description = "Nushell plugin for NATS JetStream. Provides nats pub, nats stream setup, nats status, nats notify.",
version_min = "0.110.0",
check_cmd = "plugin list | where name == 'nats' | first | get version",
install_hint = "plugin add /path/to/nu_plugin_nats # then restart nu",
severity = 'Soft,
contexts = ["local_dev"],
used_by_forms = ["new_project"],
used_by_modes = ["new_project"],
config_check = "plugin list | where name == 'nats' | is-not-empty | into string",
config_hint = "nats plugin must be registered: plugin add /path/to/nu_plugin_nats",
} | s.Tool),
# ── Optional ecosystem tooling ────────────────────────────────────────────
({
id = "kogral",
binary = "kogral",
description = "Graph database CLI. Optional — new_project continues without it.",
version_min = "0.1.0",
check_cmd = "kogral --version",
version_extract = "(\\d+\\.\\d+\\.\\d+)",
install_hint = "cargo install kogral # see: https://repo.jesusperez.pro/jesus/kogral",
severity = 'Soft,
contexts = ["local_dev"],
used_by_forms = ["new_project"],
used_by_modes = ["new_project"],
} | s.Tool),
({
id = "syntaxis",
binary = "syntaxis",
description = "Project registry CLI. Optional — new_project continues without it.",
version_min = "0.1.0",
check_cmd = "syntaxis --version",
version_extract = "(\\d+\\.\\d+\\.\\d+)",
install_hint = "cargo install syntaxis # see: https://repo.jesusperez.pro/jesus/syntaxis",
severity = 'Soft,
contexts = ["local_dev"],
used_by_forms = ["new_project"],
used_by_modes = ["new_project"],
} | s.Tool),
],
}

View File

@ -0,0 +1,48 @@
let s = import "./schema.ncl" in
# Execution contexts — named environments with different tool subsets.
# The same operation may have different requirements depending on context.
#
# Query:
# nickel export reflection/requirements/contexts.ncl | get contexts | where id == "agent"
#
# Cross-reference with tools:
# let ctx = "ci"
# nickel export reflection/requirements/base.ncl
# | get tools
# | where (contexts | any { |c| $c == $ctx })
# | where severity == 'Hard
{
contexts = [
({
id = "local_dev",
description = "Full local development environment. All interactive tools available. Used for: authoring ADRs, initializing projects, running forms interactively.",
requires = ["nickel", "nushell", "git", "typedialog"],
note = "nats-cli, kogral, syntaxis are Soft — install per project needs.",
} | s.Context),
({
id = "ci",
description = "Continuous integration environment. No interactive backends. No typedialog. Used for: validating ADR exports, running check_hints, verifying ontology exports.",
requires = ["nickel", "nushell", "git"],
note = "typedialog is not required — CI does not run interactive forms. Agent-mode queries via `nickel export` are sufficient.",
} | s.Context),
({
id = "agent",
description = "AI agent context. No interactive terminal. No typedialog interactive backend. Agent reads form NCL directly via `nickel export` and fills templates. Used for: reading form structure, extracting field definitions, generating ADR files from templates.",
requires = ["nickel", "nushell"],
note = "Agent does not need typedialog installed — it reads reflection/forms/*.ncl directly with nickel export. Nushell needed for check_hint execution.",
} | s.Context),
({
id = "benchmark",
description = "Performance benchmarking context. Same as local_dev plus Rust toolchain for cargo bench. Used for: measuring stratum-embeddings, stratum-graph, stratum-llm performance.",
requires = ["nickel", "nushell", "git", "typedialog"],
note = "Requires nightly Rust toolchain for cargo bench with criterion. Isolated from NATS/SurrealDB production instances.",
} | s.Context),
],
}

View File

@ -0,0 +1,42 @@
# Requirements schema — contract types for tool declarations and execution contexts.
# Imported by: reflection/requirements/base.ncl, reflection/requirements/contexts.ncl
#
# NOTE: This file is a contract library — do not `nickel export` it directly.
# Export base.ncl or contexts.ncl instead.
let severity_type = [| 'Hard, 'Soft |] in
# A tool or external dependency that operations in this project require.
let tool_type = {
id | String | default = "",
binary | String | default = "",
description | String | default = "",
version_min | String | default = "0.0.0",
check_cmd | String | default = "",
version_extract | String | default = "(\\d+\\.\\d+\\.\\d+)",
install_hint | String | default = "",
docs_url | String | optional,
severity | severity_type | default = 'Soft,
contexts | Array String | default = [],
used_by_forms | Array String | default = [],
used_by_modes | Array String | default = [],
config_check | String | optional,
config_hint | String | optional,
# When set, presence is checked via `plugin list` instead of `which`.
# The check_cmd is still run to extract version from `plugin list` output.
plugin_name | String | optional,
} in
# A named execution context with a subset of required tools.
let context_type = {
id | String | default = "",
description | String | default = "",
requires | Array String | default = [],
note | String | optional,
} in
{
Tool = tool_type,
Context = context_type,
Severity = severity_type,
}

78
reflection/schema.ncl Normal file
View File

@ -0,0 +1,78 @@
let _Dependency = {
step | String,
kind | [| 'Always, 'OnSuccess, 'OnFailure |] | default = 'Always,
condition | String | optional,
} in
let _OnError = {
strategy | [| 'Stop, 'Continue, 'Retry, 'Fallback, 'Branch |],
target | String | optional,
on_success | String | optional,
max | Number | default = 3,
backoff_s | Number | default = 5,
} in
let _ActionStep = fun ActionContract => {
id | String,
action | ActionContract,
depends_on | Array _Dependency | default = [],
cmd | String | optional,
actor | [| 'Human, 'Agent, 'Both |] | default = 'Both,
on_error | _OnError | default = { strategy = 'Stop },
verify | String | optional,
note | String | optional,
} in
let _ModeBase = fun ActionContract => {
id | String,
trigger | String,
preconditions | Array String | default = [],
steps | Array (_ActionStep ActionContract),
postconditions | Array String | default = [],
} in
# DAG-validated Mode contract:
# 1. structural contract via _ModeBase
# 2. step ID uniqueness within the mode
# 3. referential integrity — all depends_on.step reference an existing id
# Cycle detection is a separate Rust-side pass (ontoref-reflection::dag::validate).
let _Mode = fun ActionContract =>
std.contract.custom (fun label value =>
let validated = value | (_ModeBase ActionContract) in
let steps = validated.steps in
let ids = steps |> std.array.map (fun s => s.id) in
let _after_unique = ids |> std.array.fold_left (fun acc id =>
if std.record.has_field id acc.seen then
std.contract.blame_with_message
"Mode '%{validated.id}': duplicate step id '%{id}'"
label
else
{ seen = acc.seen & { "%{id}" = true }, ok = true }
) { seen = {}, ok = true } in
let bad_refs = steps |> std.array.flat_map (fun step =>
step.depends_on
|> std.array.filter (fun dep =>
!(ids |> std.array.any (fun i => i == dep.step))
)
|> std.array.map (fun dep =>
"step '%{step.id}' depends_on unknown '%{dep.step}'"
)
) in
if std.array.length bad_refs > 0 then
std.contract.blame_with_message
"Mode '%{validated.id}' has invalid depends_on: %{std.string.join ", " bad_refs}"
label
else
'Ok validated
)
in
{
Dependency = _Dependency,
OnError = _OnError,
ActionStep = _ActionStep,
Mode = _Mode,
}

View File

@ -0,0 +1,33 @@
let status_type = [| 'Open, 'InProgress, 'Done, 'Cancelled |] in
let priority_type = [| 'Critical, 'High, 'Medium, 'Low |] in
let kind_type = [| 'Todo, 'Wish, 'Idea, 'Bug, 'Debt |] in
let graduate_type = [| 'Adr, 'Mode, 'StateTransition, 'PrItem |] in
let item_type = {
id | String,
title | String,
kind | kind_type,
priority | priority_type | default = 'Medium,
status | status_type | default = 'Open,
detail | String | default = "",
# Optional links to existing artifacts
related_adrs | Array String | default = [],
related_modes | Array String | default = [],
related_dim | String | optional, # state.ncl dimension id
# Graduation target — when this item is ready to be promoted
graduates_to | graduate_type | optional,
# ISO date strings
created | String | default = "",
updated | String | default = "",
} in
{
Status = status_type,
Priority = priority_type,
Kind = kind_type,
GraduateTo = graduate_type,
Item = item_type,
BacklogConfig = {
items | Array item_type,
},
}

View File

@ -0,0 +1,44 @@
# Validation contracts for .coder/ entries.
# Applied after schema — enforce semantic invariants.
# Entry must have a non-empty title
let _non_empty_title = std.contract.custom (
fun label =>
fun value =>
if std.string.length (std.string.trim value.title) == 0 then
'Error {
message = "Entry: title must not be empty — extracted from first markdown heading or filename"
}
else
'Ok value
) in
# Entry must have a non-empty author
let _non_empty_author = std.contract.custom (
fun label =>
fun value =>
if std.string.length (std.string.trim value.author) == 0 then
'Error {
message = "Entry: author must not be empty — derived from workspace directory name"
}
else
'Ok value
) in
# Entry in Inbox category should not have relates_to (not yet triaged)
let _inbox_no_relations = std.contract.custom (
fun label =>
fun value =>
if value.category == 'Inbox && std.array.length value.relates_to > 0 then
'Error {
message = "Entry in Inbox should not have relates_to — triage first, then add relations"
}
else
'Ok value
) in
{
NonEmptyTitle = _non_empty_title,
NonEmptyAuthor = _non_empty_author,
InboxNoRelations = _inbox_no_relations,
}

View File

@ -0,0 +1,19 @@
let s = import "coder.ncl" in
{
make_author = fun data => s.Author & data,
make_context = fun data => s.Context & data,
make_entry = fun data => s.Entry & data,
make_record = fun data => s.Record & data,
Author = s.Author,
Context = s.Context,
Entry = s.Entry,
Record = s.Record,
RecordContext = s.RecordContext,
Kind = s.Kind,
Category = s.Category,
Domain = s.Domain,
ActorKind = s.ActorKind,
ContentRole = s.ContentRole,
}

View File

@ -0,0 +1,129 @@
let actor_kind_type = [|
'Human,
'AgentClaude,
'AgentCustom,
'CI,
|] in
let kind_type = [|
'done,
'plan,
'info,
'review,
'audit,
'commit,
'unknown,
|] in
let category_type = [|
'Inbox,
'Insight,
'Feature,
'Bugfix,
'Investigation,
'Decision,
'Review,
'Resource,
|] in
let content_role_type = [|
'ProcessMemory,
'KnowledgeBase,
'Resource,
|] in
# ── Author ──────────────────────────────────────────────────────────────────
# .coder/<author>/author.ncl
let author_type = {
name | String,
actor | actor_kind_type,
model | String | default = "",
contact | String | default = "",
} in
# ── Category context ────────────────────────────────────────────────────────
# .coder/<author>/<category>/context.ncl
let context_type = {
category | category_type,
role | content_role_type | default = 'ProcessMemory,
description | String | default = "",
graduable | Bool | default = false,
} in
# ── Entry (companion NCL) ──────────────────────────────────────────────────
# Generated by `coder triage` next to each .md file.
# {filename}.ncl is the companion for {filename}.md
#
# Extracted from:
# - filename → date, kind
# - first line of markdown → title
# - content keywords → tags
# - author workspace → author
# - triage target → category
let entry_type = {
title | String,
date | String | default = "",
author | String,
kind | kind_type | default = 'unknown,
category | category_type,
tags | Array String | default = [],
relates_to | Array String | default = [],
supersedes | String | default = "",
source | String | default = "",
} in
# ── Domain (for insights) ────────────────────────────────────────────────
let domain_type = [|
'Language,
'Runtime,
'Architecture,
'Tooling,
'Pattern,
'Debugging,
'Security,
'Performance,
|] in
# ── Record context ───────────────────────────────────────────────────────
# Captures the session context in which a record was generated.
let record_context_type = {
trigger | String | default = "",
files_touched | Array String | default = [],
} in
# ── Record (JSON entry) ─────────────────────────────────────────────────
# Generated by `coder record` — stored in entries.jsonl per category.
# Self-contained: no companion NCL needed.
# Superset of Entry: adds content, context, and optional insight fields.
let record_type = {
title | String,
date | String | default = "",
author | String,
kind | kind_type | default = 'unknown,
category | String,
tags | Array String | default = [],
relates_to | Array String | default = [],
content | String,
context | record_context_type | optional,
domain | domain_type | optional,
reusable | Bool | optional,
} in
{
ActorKind = actor_kind_type,
Kind = kind_type,
Category = category_type,
ContentRole = content_role_type,
Domain = domain_type,
Author = author_type,
Context = context_type,
Entry = entry_type,
RecordContext = record_context_type,
Record = record_type,
}

View File

@ -0,0 +1,45 @@
let profile_type = [| 'Development, 'Staging, 'Production, 'CI, 'Test, 'Custom |] in
let seal_type = {
hash | String, # sha256 of nickel export <profile.ncl> (drift detection)
snapshot_hash | String | default = "", # sha256 of values_snapshot JSON (rollback integrity)
applied_at | String, # ISO 8601 datetime
applied_by | String, # actor: developer | agent | ci
note | String | default = "",
related_adr | String | default = "", # adr-NNN
related_pr | String | default = "", # PR number or URL
related_bug | String | default = "", # backlog item id or issue ref
} in
let config_state_type = {
id | String, # cfg-<timestamp>-<actor>
profile | profile_type,
seal | seal_type,
values_snapshot | String, # JSON-encoded values at seal time (String to avoid Nickel/JSON syntax clash)
supersedes | String | default = "", # cfg-id this replaces (rollback chain)
} in
# Per-profile variant constraints — what is allowed to differ between profiles
let profile_invariants_type = {
# Fields that MUST have different values in Production vs Development
must_differ | Array String | default = [],
# Fields that MUST be identical across all profiles (shared contract)
must_match | Array String | default = [],
# Fields forbidden in Production (e.g. debug flags)
forbidden_in_production | Array String | default = [],
} in
let config_manifest_type = {
project | String,
profiles | Array profile_type,
invariants | profile_invariants_type | default = { must_differ = [], must_match = [], forbidden_in_production = [] },
active | { _: String }, # profile → cfg-NNN (current active seal id)
} in
{
Profile = profile_type,
Seal = seal_type,
ConfigState = config_state_type,
ProfileInvariants = profile_invariants_type,
ConfigManifest = config_manifest_type,
}

View File

@ -0,0 +1,28 @@
let connection_kind_type = [|
'LibraryDependency,
'DeployedBy,
'CoDeveloped,
'DataSource,
'EventConsumer,
'EventProducer,
'Monitoring,
'Unknown,
|] in
let connection_type = {
project | String,
kind | connection_kind_type,
note | String | default = "",
url | String | default = "",
} in
let connections_type = {
upstream | Array connection_type | default = [],
downstream | Array connection_type | default = [],
peers | Array connection_type | default = [],
} in
{
Connection = connection_type,
Connections = connections_type,
}

View File

@ -0,0 +1,23 @@
let domain_type = [|
'Language,
'Runtime,
'Architecture,
'Tooling,
'Pattern,
'Debugging,
'Security,
'Performance,
|] in
let insight_type = {
id | String,
domain | domain_type,
applies_to | Array String | default = [],
tags | Array String | default = [],
reusable | Bool | default = true,
} in
{
Domain = domain_type,
Insight = insight_type,
}

View File

@ -0,0 +1,38 @@
let module_system_type = [| 'Import, 'Mod, 'Hybrid, 'Flat |] in
let module_type = {
name | String,
required | Bool | default = true,
description | String | default = "",
} in
{
ModuleSystem = module_system_type,
Module = module_type,
Convention = {
system | module_system_type | default = 'Mod,
directory | String | default = "justfiles",
extension | String | default = ".just",
canonical_modules | Array module_type | default = [
{ name = "build", required = true, description = "Compilation, linking, output generation" },
{ name = "test", required = true, description = "Unit, integration, property-based tests" },
{ name = "dev", required = true, description = "Development workflow: fmt, lint, watch" },
{ name = "ci", required = true, description = "CI pipeline orchestration" },
{ name = "distro", required = false, description = "Packaging, distribution, release" },
{ name = "docs", required = false, description = "Documentation generation and serving" },
{ name = "nickel", required = false, description = "Nickel typecheck, export, validation" },
{ name = "deploy", required = false, description = "Deployment to staging/production" },
],
required_recipes | Array String | default = [
"default",
"help",
],
required_variables | Array String | default = [
"project_root",
],
},
}

View File

@ -0,0 +1,28 @@
# Plan schema — typed metadata companion to a .plan.md file.
#
# Each .plan.md generated by the compose UI has a parallel
# .plan.ncl file conforming to this schema.
#
# Load example:
# nickel export --format json 2026-03-12-new_adr.plan.ncl
let plan_status_type = [|
'Draft,
'Sent,
'Accepted,
'Executed,
'Archived,
|] in
{
Plan = {
template | String,
date | String,
provider | String | default = "",
status | plan_status_type | default = 'Draft,
linked_backlog | Array String | default = [],
linked_adrs | Array String | default = [],
fields | { _ | String | Array String } | default = {},
dag | Array { id | String, label | String, mode | String | default = "manual", dag | String | default = "" } | default = [],
},
}

19
reflection/schemas/qa.ncl Normal file
View File

@ -0,0 +1,19 @@
let qa_entry_type = {
id | String,
question | String,
answer | String,
actor | String | default = "human",
created_at | String | default = "",
tags | Array String | default = [],
related | Array String | default = [], # ontology node IDs or ADR refs
verified | Bool | default = false,
} in
let qa_store_type = {
entries | Array qa_entry_type | default = [],
} in
{
QaEntry = qa_entry_type,
QaStore = qa_store_type,
}

View File

@ -0,0 +1,16 @@
let actor_type = [| 'Developer, 'CI, 'Agent |] in
let purpose_type = [| 'Testing, 'Benchmarking, 'Demonstration, 'Integration |] in
{
Actor = actor_type,
Purpose = purpose_type,
Scenario = {
actor | actor_type | default = 'Developer,
purpose | purpose_type,
description | String | default = "",
dependencies| Array String | default = [],
run | String | default = "",
validates | Array String | default = [],
},
}

View File

@ -0,0 +1,23 @@
# Session schema — re-exports from coder.ncl (canonical source).
# FileMeta is the only session-specific type: lightweight companion NCL
# for markdown files that only need relates_to/tags/supersedes.
#
# For full entry types (Entry, Record), use coder.ncl directly.
let c = import "coder.ncl" in
let file_meta_type = {
relates_to | Array String | default = [],
tags | Array String | default = [],
supersedes | String | default = "",
} in
{
ActorKind = c.ActorKind,
Kind = c.Kind,
Category = c.Category,
ContentRole = c.ContentRole,
Author = c.Author,
Context = c.Context,
FileMeta = file_meta_type,
}

View File

@ -0,0 +1,107 @@
#!/usr/bin/env nu
# Generated by: ontoref form reflection/forms/adopt_ontoref.ncl
# Onboards {{ project_name }} at {{ project_dir }} into the ontoref protocol.
# All steps are additive — existing files are NOT overwritten.
let project_name = "{{ project_name }}"
let project_dir = "{{ project_dir }}"
let ontoref_dir = "{{ ontoref_dir }}"
def print_skip [label: string] {
print $" skip ($label) \(already exists\)"
}
def print_ok [label: string] {
print $" ok ($label)"
}
def print_fail [label: string, reason: string] {
print $" FAIL ($label): ($reason)"
}
# ── 1. .ontoref/ directory structure ─────────────────────────────────────────
mkdir $"($project_dir)/.ontoref/logs"
mkdir $"($project_dir)/.ontoref/locks"
print_ok ".ontoref/logs and .ontoref/locks created"
# ── 2. .ontoref/config.ncl ────────────────────────────────────────────────────
{% if install_config %}
let config_dest = $"($project_dir)/.ontoref/config.ncl"
if ($config_dest | path exists) {
print_skip ".ontoref/config.ncl"
} else {
let config_src = $"($ontoref_dir)/templates/ontoref-config.ncl"
let content = open --raw $config_src | str replace --all "{{ project_name }}" $project_name
$content | save $config_dest
print_ok ".ontoref/config.ncl"
}
{% endif %}
# ── 3. .ontology/ stubs ───────────────────────────────────────────────────────
{% if install_ontology_stubs %}
mkdir $"($project_dir)/.ontology"
for stub in ["core.ncl", "state.ncl", "gate.ncl"] {
let dest = $"($project_dir)/.ontology/($stub)"
if ($dest | path exists) {
print_skip $".ontology/($stub)"
} else {
let src = $"($ontoref_dir)/templates/ontology/($stub)"
let content = open --raw $src | str replace --all "{{ project_name }}" $project_name
$content | save $dest
print_ok $".ontology/($stub)"
}
}
{% endif %}
# ── 4. scripts/ontoref wrapper ────────────────────────────────────────────────
{% if install_scripts_wrapper %}
mkdir $"($project_dir)/scripts"
let wrapper_dest = $"($project_dir)/scripts/ontoref"
if ($wrapper_dest | path exists) {
print_skip "scripts/ontoref"
} else {
let wrapper_src = $"($ontoref_dir)/templates/scripts-ontoref"
let content = open --raw $wrapper_src | str replace --all "{{ ontoref_dir }}" $ontoref_dir
$content | save $wrapper_dest
do { chmod +x $wrapper_dest } | complete | ignore
print_ok "scripts/ontoref (executable)"
}
{% endif %}
# ── 5. Validate .ontology/ ────────────────────────────────────────────────────
{% if validate_after %}
print ""
print "Validating .ontology/ files..."
for stub in ["core.ncl", "state.ncl", "gate.ncl"] {
let file = $"($project_dir)/.ontology/($stub)"
if not ($file | path exists) {
print_skip $"validate .ontology/($stub) \(not installed\)"
continue
}
let result = do { ^nickel export $file } | complete
if $result.exit_code == 0 {
print_ok $"nickel export .ontology/($stub)"
} else {
print_fail $"nickel export .ontology/($stub)" $result.stderr
}
}
{% endif %}
# ── Summary ───────────────────────────────────────────────────────────────────
print ""
print $"Onboarding complete: ($project_name) at ($project_dir)"
print ""
print "Next steps:"
print " 1. Fill in .ontology/core.ncl — replace stub nodes with real invariants and tensions"
print " 2. Fill in .ontology/state.ncl — set current_state to where the project actually is"
print " 3. Review .ontoref/config.ncl — adjust log level, NATS settings if needed"
print $" 4. Add 'alias ontoref=\"./scripts/ontoref\"' to your shell profile"
print " 5. Run: ./scripts/ontoref describe project"

View File

@ -0,0 +1,39 @@
let d = import "../../adrs/defaults.ncl" in
let mk = d.make_adr in
mk {
id = "{{ id }}",
title = "{{ title }}",
status = '{{ status }},
date = "{{ date }}",
context = "{{ context | replace('"', '\\"') | replace('\n', ' ') | trim }}",
decision = "{{ decision | replace('"', '\\"') | replace('\n', ' ') | trim }}",
rationale = {{ rationale }},
consequences = {
positive = {{ consequences_positive }},
negative = {{ consequences_negative }},
},
alternatives_considered = {{ alternatives_considered }},
constraints = {{ constraints }},
{% if related_adrs and related_adrs != "" %}
related_adrs = [{{ related_adrs | split(",") | map("trim") | map("json") | join(", ") }}],
{% else %}
related_adrs = [],
{% endif %}
{% if supersedes and supersedes != "" %}
supersedes = "{{ supersedes }}",
{% endif %}
ontology_check = {
decision_string = "{{ ontology_decision_string }}",
invariants_at_risk = {{ ontology_invariants_at_risk | tojson }},
verdict = '{{ ontology_verdict }},
},
}

View File

@ -0,0 +1,5 @@
let s = import "../../onref/reflection/schemas/backlog.ncl" in
{
items = [],
} | s.BacklogConfig

View File

@ -0,0 +1,23 @@
let s = import "../../{{ ontoref_rel }}/reflection/schemas/config.ncl" in
# Production profile for {{ project_name }}.
# All values must be set explicitly — no defaults carry over from development.
# Sealed via: ontoref config apply production
{
profile = 'Production,
values = {
nickel_import_paths = [".", ".ontoref/adrs", ".ontoref/ontology/schemas", ".ontology", "adrs", "reflection"],
nats_url = "{{ nats_url }}",
surrealdb_url = "{{ surrealdb_url }}",
default_actor = "ci",
register = {
changelog = false,
adr_check = true,
ontology_sync = false,
modes_check = true,
block_on_invariant_breach = true,
},
generators = [],
},
}

Some files were not shown because too many files have changed in this diff Show More