Skip to content
Closed
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
16 changes: 8 additions & 8 deletions .trinity/seals/mcp_tool_mcp_tri_mcp.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"built_by": "t27c-bootstrap@0.5.0+1120d30ae",
"gen_hash_c": "sha256:559f17c9e2942a4e732666d74fb1079d35d1ea8f2e1e8a967b453c076f3eb595",
"gen_hash_rust": "sha256:1c820f3d29f91cd8e570b0f90709bf2a4d56b47b505f94df0880949a74145c50",
"gen_hash_verilog": "sha256:2ec9763a4c749561783d3ebb34252dc1f4a884064987bbb8d7cbd0c555d64f4d",
"gen_hash_zig": "sha256:4d499c482afffae6f97ee309682df8a914f721b30fe5ca89b6af0b91d897adb5",
"built_by": "t27c-bootstrap@0.5.2+1ab3a30aa",
"gen_hash_c": "sha256:94f2b9d84e506db530f2b6cbc1fa5ae9ceeb953a9bdbd5fdcd5a8ec1698e26cf",
"gen_hash_rust": "sha256:0dfc81535eadd2f97bb23984253f6bc8e5c41d13b415d3d7f5dfe9c5d65ce290",
"gen_hash_verilog": "sha256:eaaa0d85d10e6a39a9fce9f2a1833f7d1b5d7f0fb86b84c5c834e546560a0b48",
"gen_hash_zig": "sha256:faaea991871b395879a7f56e114e93e1119820145500c2b47ee85e0d2507a02f",
"module": "tool_mcp_tri_mcp",
"ring": 12,
"sealed_at": "2026-10-07T23:11:20Z",
"sealed_by": "t27c-bootstrap@0.5.0",
"spec_hash": "sha256:2db3e41d25dd8eff540b15459ce20402bac3d4520731a356b20a6fa8a81b9f3d",
"sealed_at": "2026-10-09T10:15:58Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:a0ff5b0e9e6f1793032f14f7a5f4baf31ea8fb6df1179e7a8fffc75a6e0f9db2",
"spec_path": "specs/tools/mcp/tri-mcp.t27",
"tests": {
"failed": 0,
Expand Down
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["bootstrap", "cli/tri", "cli/trios-bridge", "cli/flash-spi", "cli/dlc10", "cli/t27b"]
members = ["bootstrap", "cli/tri", "cli/tri-mcp", "cli/trios-bridge", "cli/flash-spi", "cli/dlc10", "cli/t27b"]
# `ffi` was in NEITHER members nor exclude, so `cargo publish` in it failed on
# manifest load -- "current package believes it's in a workspace when it's not"
# -- before it could reach a registry. That is why the crates.io leg of the
Expand Down
78 changes: 26 additions & 52 deletions cli/tri-mcp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use serde_json::Value;
use sha2::{Digest, Sha256};
use uuid::Uuid;

#[path = "../../../gen/rust/tri/test/report.rs"] #[allow(dead_code, unused_parens)]
mod report; // t27c gen-rust of specs/tri/test/report.t27 (#7369): the same reading of `t27c test-report` that `tri test` uses

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ActiveSkill {
skill_id: String,
Expand Down Expand Up @@ -226,14 +229,15 @@ fn build_tools_list() -> Vec<Value> {
}),
serde_json::json!({
"name": "tri_test",
"description": "Runs tests for a .t27 spec file using t27c.",
"description": "Runs a .t27 spec's tests with `t27c test-report`; isError unless every test passed and at least one ran (rules: specs/tri/test/report.t27).",
"inputSchema": {
"type": "object",
"properties": {
"spec_path": {
"type": "string",
"description": "Path to the .t27 spec file"
}
},
"t27c": {"type": "string", "description": "The t27c binary; default target/release/t27c, then target/debug/t27c, then t27c on PATH"}
},
"required": ["spec_path"]
}
Expand Down Expand Up @@ -722,58 +726,28 @@ fn cmd_gen(root: &Path, args: &Value) -> Result<Value> {
}
}

/// `t27c test` only lists the tests (#8140); they run in `t27c test-report`, and the verdict is report.t27's, as in `tri test`.
fn cmd_test(root: &Path, args: &Value) -> Result<Value> {
let spec_path = args
.get("spec_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("missing 'spec_path' parameter"))?;

let spec = Path::new(spec_path);
if !spec.exists() {
return Ok(tool_error_text(&format!(
"Spec file not found: {}",
spec_path
)));
}

let output = Command::new("t27c")
.args(["test", spec_path])
.current_dir(root)
.output();

match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
let success = out.status.success();

append_akashic(
root,
&make_akashic_event(
"tri.test",
if success { "success" } else { "failure" },
serde_json::json!({
"spec_path": spec_path,
"exit_code": out.status.code()
}),
),
)?;

let result = serde_json::json!({
"success": success,
"exit_code": out.status.code(),
"stdout": stdout,
"stderr": stderr,
"spec_path": spec_path
});

Ok(tool_result_text(&serde_json::to_string_pretty(&result)?))
}
Err(e) => {
let msg = format!("Failed to execute t27c: {}. Is t27c on PATH?", e);
Ok(tool_error_text(&msg))
}
let spec = args.get("spec_path").and_then(|v| v.as_str()).ok_or_else(|| anyhow::anyhow!("missing 'spec_path' parameter"))?;
if !root.join(spec).exists() {
return Ok(tool_error_text(&format!("Spec file not found: {}", spec)));
}
let built = ["target/release/t27c", "target/debug/t27c"].iter().map(|p| root.join(p)).find(|p| p.exists());
let t27c = args.get("t27c").and_then(|v| v.as_str()).map(PathBuf::from).or(built).unwrap_or_else(|| "t27c".into());
let out = Command::new(&t27c).args(["test-report", spec]).current_dir(root).output().map_err(|e| anyhow::anyhow!("cannot start `{} test-report`: {}", t27c.display(), e))?;
let text: &'static str = String::from_utf8_lossy(&out.stdout).into_owned().leak(); // gen-rust's `string` is `&'static str` (#7449)
let (v, total) = (report::verdict(text, out.status.success()), |k| report::total(text, k));
let names = |k| (0..report::count(text, k)).map(|n| &text[report::name_from(text, k, n)..report::name_to(text, k, n)]).collect::<Vec<_>>();
let outcome = if report::passes(v) { "success" } else { "failure" };
append_akashic(root, &make_akashic_event("tri.test", outcome, serde_json::json!({"spec_path": spec, "verdict": v, "exit_code": out.status.code()})))?;
let result = serde_json::json!({
"spec_path": spec, "passes": report::passes(v), "verdict": v, "why": report::why(v),
"tests": total(report::LINE_TESTS), "pass": total(report::LINE_PASS), "fail": total(report::LINE_FAIL),
"failed": names(report::LINE_FAIL_NAME), "blocked": names(report::LINE_BLOCKED),
"exit_code": out.status.code(), "report": text, "stderr": String::from_utf8_lossy(&out.stderr)
});
let shown = serde_json::to_string_pretty(&result)?;
Ok(if report::passes(v) { tool_result_text(&shown) } else { tool_error_text(&shown) })
}

fn cmd_verdict(root: &Path) -> Result<Value> {
Expand Down
40 changes: 40 additions & 0 deletions cli/tri-mcp/tests/tri_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! tri_test runs a spec's tests and reads the verdict through report.t27 (#8140): a green spec, a red copy
//! with one planted failure, and a stand-in t27c that only lists the tests, as `t27c test` did.
use serde_json::{json, Value}; use sha2::{Digest, Sha256};
use std::{io::Write, path::Path, process::{Command, Stdio}};

fn sha(p: &Path) -> Vec<u8> { Sha256::digest(std::fs::read(p).unwrap()).to_vec() }
/// One tools/call of tri_test on a server started in `root`: the result's text as JSON, and isError.
fn tri_test(root: &Path, t27c: &Path) -> (Value, bool) {
let mut s = Command::new(env!("CARGO_BIN_EXE_tri-mcp")).current_dir(root).stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().unwrap();
let args = json!({"spec_path": "gft_relu.t27", "t27c": t27c});
writeln!(s.stdin.take().unwrap(), "{}", json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "tri_test", "arguments": args}})).unwrap();
let r: Value = serde_json::from_slice(&s.wait_with_output().unwrap().stdout).unwrap();
(serde_json::from_str(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap(), r["result"]["isError"].as_bool().unwrap())
}

#[test]
fn tri_test_runs_the_tests_and_a_listing_is_not_a_pass() {
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let t27c = std::env::var_os("T27C").map(Into::into).or_else(|| ["release", "debug"].iter().map(|d| repo.join("target").join(d).join("t27c")).find(|p| p.exists()));
let t27c: std::path::PathBuf = t27c.expect("no t27c: run `cargo build -p t27c` or set T27C");
let (spec, root) = (repo.join("specs/ternary/gft_relu.t27"), std::env::temp_dir().join(format!("tri-mcp-test-{}", std::process::id())));
let (before, copy) = (sha(&spec), root.join("gft_relu.t27")); // a temp root, so the tracked akashic log stays as it is
std::fs::create_dir_all(root.join(".trinity")).unwrap();
std::fs::copy(&spec, &copy).unwrap();
let (green, err) = tri_test(&root, &t27c);
assert!(!err && green["passes"] == true && green["tests"] == 4 && green["pass"] == 4 && green["fail"] == 0, "{green}");
let src = std::fs::read_to_string(&copy).unwrap();
std::fs::write(&copy, src.replacen("{ return 0; } // negative", "{ return x; } // negative", 1)).unwrap(); // #7400's plant
let (red, err) = tri_test(&root, &t27c);
assert!(err && red["passes"] == false && red["tests"] == 4 && red["pass"] == 3 && red["fail"] == 1 && red["failed"] == json!(["negz"]), "{red}");
std::fs::write(&copy, &src).unwrap();
assert_eq!(sha(&copy), before, "the copy is restored byte for byte");
let stub = |name: &str, body: &str| { let p = root.join(name); assert!(Command::new("sh").args(["-c", "printf '%s' \"$2\" > \"$1\" && chmod +x \"$1\"", "sh"]).arg(&p).arg(body).status().unwrap().success()); p };
let (listed, err) = tri_test(&root, &stub("lists-only", "#!/bin/sh\necho listed 4 tests\n")); // negative control: what `t27c test` gives
assert!(err && listed["passes"] == false && listed["verdict"] == 4 && listed["tests"] == 0, "{listed}");
let (torn, err) = tri_test(&root, &stub("green-exit-1", "#!/bin/sh\nprintf '\\n tests 4\\n pass 4\\n FAIL 0\\n'\nexit 1\n"));
assert!(err && torn["passes"] == false && torn["verdict"] == 7 && torn["tests"] == 4, "{torn}"); // a green report from a failed run
std::fs::remove_dir_all(&root).unwrap();
assert_eq!(sha(&spec), before, "the spec in the tree is untouched");
}
1 change: 1 addition & 0 deletions docs/reports/orphan_modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"ceilings": {
"bootstrap": 7,
"cli/tri": 0,
"cli/tri-mcp": 0,
"cli/trios-bridge": 0,
"cli/flash-spi": 0,
"cli/dlc10": 0,
Expand Down
8 changes: 5 additions & 3 deletions specs/tools/mcp/tri-mcp.t27
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
; specs/tools/mcp/tri-mcp.t27 -- tool mcp/tri-mcp, MCP server "tri-mcp" (gHashTag/t27)
; Source of truth for the tool card on t27.ai (#/tools?tool=mcp/tri-mcp).
; Tool names, descriptions and input keys are read from the server code or manifest named in
; SOURCE (witness source-parse, gHashTag/t27 @ 3cb7a4352492); nothing is typed by hand.
; SOURCE (witness source-parse, gHashTag/t27 @ 3cb7a4352492; tri_test's description and its t27c input
; were read from the branch that makes it run the tests, gHashTag/t27#8140. Before it, tri_test ran
; `t27c test`, which only lists the tests, and reported success whatever they did); nothing is typed by hand.
; Layer 5 of the ladder Specs -> Skills -> Crons -> Agents -> Tools. ASCII only (L3).
; phi^2 + 1/phi^2 = 3 | TRINITY

Expand All @@ -27,8 +29,8 @@ pub const ABOUT : str = "Rust MCP server exposing the PHI LOOP tri commands as t
pub const ABOUT_SOURCE : str = "derived from the tool descriptions in cli/tri-mcp/src/main.rs build_tools_list";
; Tools as the server lists them (tools/list), their descriptions and inputSchema property keys.
pub const TOOLS : [10]str = ["tri_status", "tri_skill_begin", "tri_skill_end", "tri_cell_checkpoint", "tri_cell_seal", "tri_gen", "tri_test", "tri_verdict", "tri_experience_save", "tri_health"];
pub const TOOLS_ABOUT : [10]str = ["Returns PHI LOOP status including active skill, cell, issue binding, and uncommitted changes.", "Begins a new PHI LOOP skill session bound to an issue.", "Ends the active PHI LOOP skill session.", "Records a checkpoint step in the active cell.", "Seals the active cell by computing a SHA-256 hash of its checkpoint history.", "Generates backends from a .t27 spec file using t27c.", "Runs tests for a .t27 spec file using t27c.", "Checks for toxic regressions by analyzing swarm health metrics.", "Saves an experience episode to the akashic log.", "Returns queen and swarm health status from .trinity state."];
pub const TOOLS_INPUTS : [10]str = ["", "issue,description", "", "step", "", "spec_path", "spec_path", "", "episode_type,summary,ring", ""];
pub const TOOLS_ABOUT : [10]str = ["Returns PHI LOOP status including active skill, cell, issue binding, and uncommitted changes.", "Begins a new PHI LOOP skill session bound to an issue.", "Ends the active PHI LOOP skill session.", "Records a checkpoint step in the active cell.", "Seals the active cell by computing a SHA-256 hash of its checkpoint history.", "Generates backends from a .t27 spec file using t27c.", "Runs a .t27 spec's tests with `t27c test-report`; isError unless every test passed and at least one ran (rules: specs/tri/test/report.t27).", "Checks for toxic regressions by analyzing swarm health metrics.", "Saves an experience episode to the akashic log.", "Returns queen and swarm health status from .trinity state."];
pub const TOOLS_INPUTS : [10]str = ["", "issue,description", "", "step", "", "spec_path", "spec_path,t27c", "", "episode_type,summary,ring", ""];
pub const RESOURCES : [0]str = [];
pub const RESOURCES_ABOUT : [0]str = [];
; Why TOOLS may be empty or differ from other declarations (external package, dynamic list).
Expand Down
Loading