Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,23 @@
# ═════════════════════════════════════════════════════════════════════════════════════════

# Experience logs are append-only JSONL — concatenate both sides on merge
.trinity/experience/*.jsonl merge=append-log
# `append-log` is not a built-in driver: it needs a merge.append-log.driver
# entry in .git/config, and nothing in this checkout installs one — so in
# every fresh clone this line does nothing and the file conflicts normally.
# For append-only JSONL, `union` is that behaviour and is built in.
.trinity/experience/*.jsonl merge=union

# NOW.md: canonical location is docs/NOW.md
# Root NOW.md is a symlink to docs/NOW.md — prefer incoming version
NOW.md merge=theirs
# NOW.md: canonical location is docs/NOW.md. The root file is a symlink, so
# git merges the link target, not the text — and `theirs` is not a built-in
# driver either (same missing .git/config entry as append-log had). Point the
# symlink's rule at the same built-in the real file uses; if the symlink is
# ever replaced by a real file, this keeps working.
NOW.md merge=union

# The rule above names the SYMLINK, not the file that actually conflicts, and
# `theirs` is not a built-in driver — it needs a per-clone .git/config entry
# that a fresh checkout does not have. So docs/NOW.md conflicted on every
# branch (seven times in one campaign), each resolved identically by hand:
# keep both entries. `union` is built in, so it works in every clone with no
# setup, and it is exactly that resolution.
docs/NOW.md merge=union
13 changes: 9 additions & 4 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ cd "$ROOT"
# ===== NOW.md Gate =====
bash "$ROOT/scripts/tri" check-now

if ! git diff --cached --name-only | grep -q '^NOW.md$'; then
if git diff --name-only | grep -q '^NOW.md$'; then
# The canonical file is docs/NOW.md — this gate used to match '^NOW.md$'
# only, so every commit that correctly updated docs/NOW.md still printed the
# warning, and the root file (a regular 390-line file, not the symlink the
# .gitattributes comment describes) has not been touched since 2026-08-09.
# Accept either path; the root copy's fate is the owner's call, not a hook's.
if ! git diff --cached --name-only | grep -qE '^(docs/)?NOW\.md$'; then
if git diff --name-only | grep -qE '^(docs/)?NOW\.md$'; then
echo ""
echo "⚠️ WARNING: NOW.md is modified but NOT staged."
echo " Run: git add NOW.md"
echo " Or: stage and commit NOW.md together with your changes."
echo " Run: git add docs/NOW.md"
echo " Or: stage and commit it together with your changes."
echo ""
fi
fi
Expand Down
25 changes: 25 additions & 0 deletions .tri/environments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"_comment": "Which environments each capability claim rests on. Checked by `tri fleet asof --from .tri/environments.json`. A claim listed here can be re-verified in one command instead of being repeated on faith.",
"claims": [
{
"name": "t27.ai publishes the project's writing",
"urls": ["https://t27.ai", "https://gHashTag.github.io"]
},
{
"name": "the hiking site is serving",
"urls": ["https://floripahikegpro.vercel.app"]
},
{
"name": "GitHub hosts the repositories this work is pushed to",
"urls": ["https://github.com/gHashTag/t27", "https://github.com/gHashTag/trinity-fpga"]
},
{
"name": "the FPGA fleet can be flashed and measured from software",
"needs_hardware": true
},
{
"name": "the 3-board inference cluster can be re-run",
"needs_hardware": true
}
]
}
321 changes: 321 additions & 0 deletions cli/tri/src/fleet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
//! `tri fleet` — is the hardware this plan assumes actually attached?
//!
//! Written after an audit found three present-tense capability claims in the
//! project's own notes — "3-board inference cluster PROVEN", "all three flash
//! from software without replugging", "on-chip training PROVEN" — while not a
//! single board was on the bus. Each was true when measured. None was true
//! that day.
//!
//! A measurement stays true because it happened; a capability quietly becomes
//! false when the environment regresses, and nothing announces it. The notes
//! even said "a configured fleet is a perishable measurement" — the instinct
//! was recorded and never turned into a check. This is that check.
//!
//! It refuses to guess: an empty bus is reported as an empty bus, with the
//! sentence to send the owner, not as a problem to code around.

use anyhow::{Context, Result};
use clap::Subcommand;
use std::path::PathBuf;
use std::process::Command;

#[derive(Subcommand)]
pub enum FleetCmd {
/// Scan the USB bus and say plainly what hardware is present.
Scan {
/// How many boards the plan expects. Non-zero exit if fewer are found.
#[arg(long)]
expect: Option<usize>,
},
/// Check the environments a claim depends on before repeating the claim.
///
/// The bus is one environment; a deployed site is another. Both go stale
/// the same way — the note says "works" because it worked, and nothing
/// announces the regression. This checks each named URL and the bus, and
/// reports which capability claims are currently unverifiable.
Asof {
/// URLs the claim depends on, repeatable. Checked with a HEAD request.
#[arg(long = "url")]
urls: Vec<String>,
/// Also require hardware on the bus.
#[arg(long)]
needs_hardware: bool,
/// Read the claims from a declaration instead of the command line.
///
/// Remembering which URLs back which claim is exactly the step that
/// gets skipped, so the claims live in the repository next to the
/// code they describe. Format:
///
/// {"claims":[{"name":"...","urls":["..."],"needs_hardware":false}]}
#[arg(long)]
from: Option<PathBuf>,
},
}

#[derive(serde::Deserialize)]
struct Claim {
name: String,
#[serde(default)]
urls: Vec<String>,
#[serde(default)]
needs_hardware: bool,
}

#[derive(serde::Deserialize)]
struct Claims {
claims: Vec<Claim>,
}

pub fn run(cmd: &FleetCmd) -> Result<()> {
match cmd {
FleetCmd::Scan { expect } => scan(*expect),
FleetCmd::Asof {
urls,
needs_hardware,
from,
} => match from {
Some(path) => asof_declared(path),
None => asof(urls, *needs_hardware),
},
}
}

/// Check every claim in a declaration and report per claim, because "is the
/// project fine" has no answer — each claim rests on its own environments and
/// they fail independently.
fn asof_declared(path: &PathBuf) -> Result<()> {
let text = std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
let decl: Claims =
serde_json::from_str(&text).with_context(|| format!("parse {}", path.display()))?;

let mut stale: Vec<String> = Vec::new();
for c in &decl.claims {
let mut missing: Vec<String> = Vec::new();
for u in &c.urls {
let (ok, code) = head(u);
if !ok {
missing.push(format!("{u} (HTTP {code})"));
}
}
if c.needs_hardware {
match probe_usb() {
Ok(b) if !b.is_empty() => {}
Ok(_) => missing.push("hardware (bus empty)".to_string()),
Err(_) => missing.push("hardware (cannot tell)".to_string()),
}
}
if missing.is_empty() {
println!("SAYABLE {}", c.name);
} else {
println!("STALE {}", c.name);
for m in &missing {
println!(" needs {m}");
}
stale.push(c.name.clone());
}
}

println!();
if stale.is_empty() {
println!(
"VERDICT: all {} claim(s) rest on reachable environments.",
decl.claims.len()
);
return Ok(());
}
println!(
"VERDICT: {} of {} claim(s) may NOT be stated in the present tense today.",
stale.len(),
decl.claims.len()
);
anyhow::bail!("{} stale claim(s)", stale.len())
}

/// HEAD one URL. A timeout is a failure to verify, not a failure of the site —
/// the two are reported differently because only one of them is the owner's
/// problem.
fn head(url: &str) -> (bool, String) {
let out = Command::new("curl")
.args([
"-s",
"-o",
"/dev/null",
"-w",
"%{http_code}",
"-m",
"15",
"-L",
"-I",
url,
])
.output();
match out {
Ok(o) => {
let code = String::from_utf8_lossy(&o.stdout).trim().to_string();
let ok = code.starts_with('2') || code.starts_with('3');
(ok, code)
}
Err(e) => (false, format!("curl failed: {e}")),
}
}

fn asof(urls: &[String], needs_hardware: bool) -> Result<()> {
let mut unverifiable: Vec<String> = Vec::new();

for u in urls {
let (ok, code) = head(u);
println!("{:<7} {u}", if ok { "live" } else { "DOWN" });
if !ok {
unverifiable.push(format!("{u} (HTTP {code})"));
}
}

if needs_hardware {
match probe_usb() {
Ok(b) if !b.is_empty() => println!("{:<7} {} USB bridge(s)", "live", b.len()),
Ok(_) => {
println!("{:<7} no board on the bus", "DOWN");
unverifiable.push("hardware (bus empty)".to_string());
}
Err(e) => {
println!("{:<7} {e}", "UNKNOWN");
unverifiable.push("hardware (cannot tell)".to_string());
}
}
}

println!();
if unverifiable.is_empty() {
println!("VERDICT: every environment this claim depends on is reachable.");
println!("The claim can be repeated in the present tense today.");
return Ok(());
}

println!(
"VERDICT: {} environment(s) unreachable:",
unverifiable.len()
);
for u in &unverifiable {
println!(" {u}");
}
println!();
println!("Any note asserting this capability in the present tense is a MEASUREMENT");
println!("of the past, not a capability of today. Re-verify before repeating it.");
anyhow::bail!("{} environment(s) unverifiable", unverifiable.len())
}

/// One JTAG/UART bridge as the bus reports it.
struct Bridge {
kind: String,
serial: Option<String>,
}

/// Read the USB tree. macOS only — on anything else this says so rather than
/// reporting an empty fleet, because "no boards" and "cannot tell" are
/// different answers and only one of them is safe to act on.
fn probe_usb() -> Result<Vec<Bridge>> {
if !cfg!(target_os = "macos") {
anyhow::bail!("bus probe is implemented for macOS only; cannot tell what is attached");
}
let out = Command::new("ioreg")
.args(["-p", "IOUSB", "-l", "-w0"])
.output()
.context("ioreg is not available")?;
let text = String::from_utf8_lossy(&out.stdout);

let mut found = Vec::new();
let mut pending: Option<String> = None;
for line in text.lines() {
let l = line.trim();
// Device nodes appear as `+-o <name>@<addr>`; the serial follows in
// that node's property block a few lines later.
if l.starts_with("+-o") {
let name = l.trim_start_matches("+-o").trim();
let lower = name.to_ascii_lowercase();
pending = if lower.contains("ft232")
|| lower.contains("ftdi")
|| lower.contains("usb serial")
|| lower.contains("jtag")
{
Some(name.split('@').next().unwrap_or(name).trim().to_string())
} else {
None
};
if let Some(kind) = pending.clone() {
found.push(Bridge { kind, serial: None });
}
} else if pending.is_some() && l.contains("\"USB Serial Number\"") {
if let Some(v) = l.split('=').nth(1) {
if let Some(b) = found.last_mut() {
b.serial = Some(v.trim().trim_matches('"').to_string());
}
}
pending = None;
}
}
Ok(found)
}

/// Serial device nodes, which is what a UART console actually needs. Bluetooth
/// and the debug console are always present and are not hardware.
fn probe_tty() -> Vec<String> {
let mut nodes = Vec::new();
if let Ok(dir) = std::fs::read_dir("/dev") {
for e in dir.flatten() {
let name = e.file_name().to_string_lossy().to_string();
if (name.starts_with("cu.") || name.starts_with("tty."))
&& !name.contains("Bluetooth")
&& !name.contains("debug-console")
{
nodes.push(name);
}
}
}
nodes.sort();
nodes
}

fn scan(expect: Option<usize>) -> Result<()> {
let bridges = probe_usb()?;
let ttys = probe_tty();

println!("USB JTAG/UART bridges: {}", bridges.len());
for b in &bridges {
match &b.serial {
Some(s) => println!(" {} (serial {s})", b.kind),
None => println!(" {}", b.kind),
}
}
println!("serial device nodes: {}", ttys.len());
for t in &ttys {
println!(" /dev/{t}");
}
println!();

if bridges.is_empty() && ttys.is_empty() {
println!("VERDICT: no hardware attached.");
println!();
println!("Any 'proven on hardware' note in this project is a MEASUREMENT of the past,");
println!("not a capability of today. Do not write code for a board that is not there,");
println!("and do not report readiness to flash. Tell the owner instead:");
println!();
println!(" \"No board is on the bus — the fleet needs to be plugged in before");
println!(" anything hardware-side can run. This needs hands, not code.\"");
} else {
println!(
"VERDICT: {} bridge(s), {} serial node(s) present.",
bridges.len(),
ttys.len()
);
}

if let Some(n) = expect {
if bridges.len() < n {
anyhow::bail!(
"expected {n} board(s), found {} — refusing to report a fleet that is not there",
bridges.len()
);
}
}
Ok(())
}
Loading
Loading