diff --git a/taicpp/main.cpp b/taicpp/main.cpp index fd3acd6..f2105ed 100644 --- a/taicpp/main.cpp +++ b/taicpp/main.cpp @@ -8,6 +8,7 @@ #include int main(int argc, char** argv){ detz::cli::Parser p; auto cmd = p.parse(argc, argv); + if(!cmd.opts.error.empty()){ std::fputs((cmd.opts.error + "\n").c_str(), stderr); return 2; } if(cmd.opts.version){ std::puts(("tai " + std::string(detz::kVersion)).c_str()); return 0; } if(cmd.opts.cap_manifest){ std::fputs("# tai CAP agent manifest\n", stdout); return 0; } if(cmd.name=="run" && !cmd.args.empty()){ diff --git a/taicpp/src/cli/cli.cpp b/taicpp/src/cli/cli.cpp index 5121c2c..206081c 100644 --- a/taicpp/src/cli/cli.cpp +++ b/taicpp/src/cli/cli.cpp @@ -5,7 +5,6 @@ namespace detz::cli { std::string Parser::helpText() { return std::string( - std::string( "usage: tai [-h] [--profile PROFILE] [--yes] [--yolo] [--auto] [--continue]\n" " [--debug [SINK]] [--debug-format FORMAT] [--env ENV]\n" " [--no-opencode] [--update-models] [--eval-retrieval] [--stats-rag]\n" @@ -88,8 +87,7 @@ std::string Parser::helpText() { " --tui-local run the fullscreen Telegram-style chat UI on the local\n" " vault, no server needed\n" " --theme NAME TUI theme: dark, light, nord, gruvbox, ansi (or\n" - " TAI_THEME)\n") - + std::string("\n version ") + std::string(kVersion) + std::string("\n")); + " TAI_THEME)\n"); } @@ -105,8 +103,8 @@ Options Parser::parseOpts(int argc, char** argv) { else if (a=="--yolo") o.yolo = true; else if (a=="--auto") o.auto_ = true; else if (a=="--continue") o.resume = true; - else if (a=="--debug") { o.debug = (i+1{}; while (++ipush_back(argv[i]); break; } else if (a=="--serve-mcp") nxt(o.serve_mcp); @@ -146,20 +144,22 @@ Options Parser::parseOpts(int argc, char** argv) { else if (a=="--tui-local") o.tui_local = true; else if (a=="--theme") nxt(o.theme); else if (a=="--help"||a=="-h") o.help = true; - else if (!a.empty() && a[0]=='-') { /* unknown flag: ignore */ } + else if (!a.empty() && a[0]=='-') { if (o.error.empty()) o.error = "tai: error: unrecognized arguments: " + a; } else o.prompt.push_back(a); } return o; } Command Parser::parse(int argc, char** argv) { Command c; c.opts = parseOpts(argc, argv); - if (c.opts.help || argc<=1) c.name = "--help"; + if (!c.opts.error.empty()) c.name = "--error"; + else if (c.opts.help || argc<=1) c.name = "--help"; else if (c.opts.version) c.name = "--version"; else c.name = "run"; for (int i=1;i allow_tool, deny_tool; bool help = false; + std::string error; // non-empty => argparse-style parse failure, exit 2 std::vector prompt; }; struct Command { std::string name{"--help"}; std::vector args; Options opts; int run(); }; diff --git a/taicpp/tools/parity_ledger.py b/taicpp/tools/parity_ledger.py new file mode 100644 index 0000000..d987fa6 --- /dev/null +++ b/taicpp/tools/parity_ledger.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Parity ledger: every tai.py symbol against the C++ tree. + +Usage: tools/parity_ledger.py [--check] (writes build/parity/ledger.json and ledger.md) + +Status per symbol: + missing no identifier in src/ or main.cpp matches the normalized name + name-only an identifier matches; behavior is NOT verified by this tool +Verification (diff of real outputs against tai.py) is a separate, later status +and is never inferred here. --check exits 1 while any symbol is missing. +""" +import ast +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +PY = os.path.join(os.path.dirname(ROOT), "tai.py") +OUT = os.path.join(ROOT, "build", "parity") + + +def norm(name): + return re.sub(r"[^a-z0-9]", "", name.lower()) + + +def cpp_idents(): + idents = set() + files = [os.path.join(ROOT, "main.cpp")] + for base, _dirs, names in os.walk(os.path.join(ROOT, "src")): + files += [os.path.join(base, n) for n in names if n.endswith((".cpp", ".h", ".hpp"))] + for path in files: + with open(path, encoding="utf-8", errors="replace") as fh: + idents.update(norm(t) for t in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", fh.read())) + return idents + + +def python_symbols(tree): + out = [] + for node in tree.body: + if isinstance(node, ast.ClassDef): + out.append(("class", node.name, node.lineno)) + for sub in node.body: + if isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef)): + out.append(("method", "%s.%s" % (node.name, sub.name), sub.lineno)) + elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + out.append(("function", node.name, node.lineno)) + return out + + +def tool_names(tree): + names = [] + for node in ast.walk(tree): + if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "tool_schema" and node.args: + arg = node.args[0] + if isinstance(arg, ast.Constant) and isinstance(arg.value, str): + names.append((arg.value, node.lineno)) + return names + + +def flag_names(tree): + flags = [] + for node in ast.walk(tree): + if isinstance(node, ast.Call) and getattr(node.func, "attr", "") == "add_argument": + for arg in node.args: + if isinstance(arg, ast.Constant) and isinstance(arg.value, str) and arg.value.startswith("--"): + flags.append((arg.value, node.lineno)) + return flags + + +def build(): + with open(PY, encoding="utf-8") as fh: + tree = ast.parse(fh.read()) + idents = cpp_idents() + rows = [] + for kind, name, line in python_symbols(tree): + leaf = name.split(".")[-1] + hit = norm(name.replace(".", "")) in idents or (kind != "class" and len(norm(leaf)) > 5 and norm(leaf) in idents) + rows.append({"kind": kind, "name": name, "line": line, "status": "name-only" if hit else "missing"}) + for name, line in tool_names(tree): + rows.append({"kind": "tool", "name": name, "line": line, "status": "name-only" if norm(name) in idents else "missing"}) + for name, line in flag_names(tree): + rows.append({"kind": "flag", "name": name, "line": line, "status": "name-only" if norm(name) in idents else "missing"}) + return rows + + +def main(): + rows = build() + os.makedirs(OUT, exist_ok=True) + with open(os.path.join(OUT, "ledger.json"), "w") as fh: + json.dump(rows, fh, indent=1) + kinds = sorted({r["kind"] for r in rows}) + lines = ["# Parity ledger (generated, do not edit)", "", + "Source: tai.py. `name-only` means an identifier matches, NOT that behavior is verified.", "", + "| kind | total | name-only | missing |", "|---|---|---|---|"] + for kind in kinds: + sub = [r for r in rows if r["kind"] == kind] + have = sum(1 for r in sub if r["status"] == "name-only") + lines.append("| %s | %d | %d | %d |" % (kind, len(sub), have, len(sub) - have)) + total_missing = sum(1 for r in rows if r["status"] == "missing") + lines += ["", "Total symbols: %d, missing: %d, verified: 0" % (len(rows), total_missing)] + with open(os.path.join(OUT, "ledger.md"), "w") as fh: + fh.write("\n".join(lines) + "\n") + print("\n".join(lines)) + return 1 if "--check" in sys.argv and total_missing else 0 + + +if __name__ == "__main__": + sys.exit(main())