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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ this file is the canonical cumulative record.
and attestation documents from the packaged registry and policy, optionally
executes a real dry-run (`--run`), and leaves a verifiable run directory.
No provider is contacted.
- After `torq demo --run`, a note on standard error names the dry run, the run
id, and the exact `torq evidence verify` command. The JSON on standard
output is unchanged. When `torq evidence verify` or `torq fleet` is pointed
at a folder of runs, standard output is one JSON object (`status`
`run_folder`, the run ids, and one verify command) and the same note is
repeated on standard error. That case does not report `evidence_missing` or
`evidence_unstable`. `torq --help` gives each command a one-line description.

### Changed
- README rewritten around the visitor: pitch, comparison table, quickstart,
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ torq demo --goal "Add input validation to the login form" --run
torq evidence verify --run-root ./torq-demo-runs/<run-id>
```

`torq demo` scaffolds the identity and attestation documents, runs a real four-stage dry-run (G1D → G1R → Builder → G2A), and leaves a verifiable run directory behind. No provider is contacted and nothing outside the run root is written. The full install matrix is in [docs/install.md](docs/install.md).
`torq demo` scaffolds the identity and attestation documents, runs a real four-stage dry-run (G1D → G1R → Builder → G2A), and leaves a verifiable run directory behind. No provider is contacted and nothing outside the run root is written. After `--run`, a note on standard error gives the run id and the exact `torq evidence verify` command; the JSON lines on standard output stay the same. Pointing `torq evidence verify` or `torq fleet` at the folder that holds the runs prints one JSON object on standard output (`status` `run_folder`, the run ids, and one verify command) and the same note on standard error. The full install matrix is in [docs/install.md](docs/install.md).

## What it looks like

Expand All @@ -50,6 +50,9 @@ $ torq demo --goal "Add input validation to the login form" --run
{"status": "dry_run_complete", "report": {"mode": "dry_run", "verdict": "dry_run_complete",
"planned_roles": ["g1d", "g1r", "builder", "g2a", "refine_bug", "refine_ui"],
"attested": true, "receipts": "torq-demo-runs/run-0d04c989291241d58830a964ce578fc9", ...}}
No AI provider was contacted (dry run). No project files were changed.
Run id: run-0d04c989291241d58830a964ce578fc9
torq evidence verify --run-root torq-demo-runs/run-0d04c989291241d58830a964ce578fc9

$ torq evidence verify --run-root torq-demo-runs/run-0d04c989291241d58830a964ce578fc9
{"finding": null, "status": "verified"}
Expand Down
5 changes: 4 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ torq demo --goal "Add input validation to the login form" --run
torq evidence verify --run-root ./torq-demo-runs/<run-id>
```

`torq demo` scaffolds the identity and attestation documents, runs a real four-stage dry-run (G1D → G1R → Builder → G2A), and leaves a verifiable run directory behind. No provider is contacted and nothing outside the run root is written. The full install matrix is in [docs/install.md](docs/install.md).
`torq demo` scaffolds the identity and attestation documents, runs a real four-stage dry-run (G1D → G1R → Builder → G2A), and leaves a verifiable run directory behind. No provider is contacted and nothing outside the run root is written. After `--run`, a note on standard error gives the run id and the exact `torq evidence verify` command; the JSON lines on standard output stay the same. Pointing `torq evidence verify` or `torq fleet` at the folder that holds the runs prints one JSON object on standard output (`status` `run_folder`, the run ids, and one verify command) and the same note on standard error. The full install matrix is in [docs/install.md](docs/install.md).

## What it looks like

Expand All @@ -42,6 +42,9 @@ $ torq demo --goal "Add input validation to the login form" --run
{"status": "dry_run_complete", "report": {"mode": "dry_run", "verdict": "dry_run_complete",
"planned_roles": ["g1d", "g1r", "builder", "g2a", "refine_bug", "refine_ui"],
"attested": true, "receipts": "torq-demo-runs/run-0d04c989291241d58830a964ce578fc9", ...}}
No AI provider was contacted (dry run). No project files were changed.
Run id: run-0d04c989291241d58830a964ce578fc9
torq evidence verify --run-root torq-demo-runs/run-0d04c989291241d58830a964ce578fc9

$ torq evidence verify --run-root torq-demo-runs/run-0d04c989291241d58830a964ce578fc9
{"finding": null, "status": "verified"}
Expand Down
1 change: 1 addition & 0 deletions docs/releases/torq-cli-v0.2.0-user-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and backed by a verifiable receipt chain.
- **One-command trial.** `torq demo --run` scaffolds a valid identity and
attestation, executes a real dry-run, and leaves a run directory that
`torq evidence verify` confirms — no provider calls, no hand-written JSON.
The dry run prints that verify command on standard error.
- **Platform honesty.** Commands report exactly what is production-grade and
what is not; `torq trust readiness` names the signing and anchoring gaps
still open.
Expand Down
21 changes: 21 additions & 0 deletions src/torq_cli/application/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from __future__ import annotations

import json
import shlex
import sys
from dataclasses import asdict
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -69,6 +71,22 @@ def execute_demo(run_root: Path, goal: str) -> dict[str, Any]:
)


def _write_dry_run_note(report: dict[str, Any]) -> None:
"""Tell a person what the dry run did, without touching stdout JSON."""
run_id = report.get("run_id")
receipts = report.get("receipts")
if not isinstance(run_id, str) or not isinstance(receipts, str):
return
command = "torq evidence verify --run-root " + shlex.quote(receipts)
note = (
"No AI provider was contacted (dry run). No project files were changed.\n"
f"Run id: {run_id}\n"
f"{command}\n"
)
sys.stderr.write(note)
sys.stderr.flush()


def demo_command(goal: str, run_root: Path, execute: bool) -> int:
"""CLI entry point. Prints JSON reports like the other commands."""
goal = goal or _DEFAULT_GOAL
Expand All @@ -95,6 +113,9 @@ def demo_command(goal: str, run_root: Path, execute: bool) -> int:
return 0
report = execute_demo(run_root, goal)
print(json.dumps({"status": "dry_run_complete", "report": report}, sort_keys=True))
# The note is on stderr so the two stdout JSON lines stay byte-compatible.
sys.stdout.flush()
_write_dry_run_note(report)
return 0
except Exception as exc:
print(json.dumps({"status": "blocked", "finding": str(exc)}, sort_keys=True))
Expand Down
77 changes: 67 additions & 10 deletions src/torq_cli/interfaces/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import argparse
import getpass
import json
import shlex
import shutil
import sys
import tempfile
Expand Down Expand Up @@ -101,22 +102,22 @@ def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="torq")
parser.add_argument("--version", action="version", version=f"torq {__version__}")
sub = parser.add_subparsers(dest="command", required=True)
profile = sub.add_parser("profile")
profile = sub.add_parser("profile", help="Validate a role profile")
profile_sub = profile.add_subparsers(dest="profile_command", required=True)
validate = profile_sub.add_parser("validate")
validate.add_argument("--config", required=True)
status = sub.add_parser("status")
status = sub.add_parser("status", help="Attest machine and role status")
status.add_argument("--offline", action="store_true")
status.add_argument("--config", required=True)
status.add_argument("--require-effective", action="store_true")
status.add_argument("--runtime")
config = sub.add_parser("config")
config = sub.add_parser("config", help="Import Console V5 configuration")
config_sub = config.add_subparsers(dest="config_command", required=True)
import_v5 = config_sub.add_parser("import-v5-normalized")
import_v5.add_argument("--config", required=True)
import_v5_console_parser = config_sub.add_parser("import-v5-console")
import_v5_console_parser.add_argument("--config", required=True)
auth = sub.add_parser("auth")
auth = sub.add_parser("auth", help="Store and check provider credentials")
auth_sub = auth.add_subparsers(dest="auth_command", required=True)
auth_status_parser = auth_sub.add_parser("status")
auth_status_parser.add_argument("--credential-file")
Expand All @@ -129,16 +130,16 @@ def _parser() -> argparse.ArgumentParser:
default="platform_keychain",
)
native.add_argument("--store-root")
harness = sub.add_parser("harness")
harness = sub.add_parser("harness", help="Inspect the live harness")
harness_sub = harness.add_subparsers(dest="harness_command", required=True)
inspect = harness_sub.add_parser("inspect")
inspect.add_argument("--expected", required=True)
inspect.add_argument("--actual", required=True)
setup = sub.add_parser("setup")
setup = sub.add_parser("setup", help="Write local config from answers")
setup.add_argument("--config", required=True)
setup.add_argument("--answers", required=True)
setup.add_argument("--credential-file")
run = sub.add_parser("run")
run = sub.add_parser("run", help="Run a governed plan, dry-run by default")
run.add_argument("--goal")
run.add_argument("--resume")
run.add_argument("--run-root", required=True)
Expand All @@ -149,12 +150,12 @@ def _parser() -> argparse.ArgumentParser:
run.add_argument("--live", action="store_true")
run.add_argument("--allow-live", action="store_true")
run.add_argument("--policy-allow-live", action="store_true")
evidence = sub.add_parser("evidence")
evidence = sub.add_parser("evidence", help="Verify a run's receipt chain")
evidence_sub = evidence.add_subparsers(dest="evidence_command", required=True)
verify = evidence_sub.add_parser("verify")
verify.add_argument("--run-root", required=True)
verify.add_argument("--trusted-public-key")
fleet = sub.add_parser("fleet")
fleet = sub.add_parser("fleet", help="Show the evidence-backed fleet view for one run")
fleet.add_argument("--run-root")
fleet.add_argument("--trusted-public-key")
fleet.add_argument("--serve", action="store_true")
Expand All @@ -180,7 +181,7 @@ def _parser() -> argparse.ArgumentParser:
demo.add_argument("--goal", default="")
demo.add_argument("--run-root", default="./torq-demo-runs")
demo.add_argument("--run", action="store_true", help="execute the dry-run after scaffolding")
trust = sub.add_parser("trust")
trust = sub.add_parser("trust", help="Report production-trust gaps")
trust_sub = trust.add_subparsers(dest="trust_command", required=True)
trust_sub.add_parser("readiness")
return parser
Expand Down Expand Up @@ -332,6 +333,55 @@ def _handle_run(args: argparse.Namespace) -> int:
return 5


def _run_collection(root: Path) -> tuple[list[str], str] | None:
"""Return run ids and one verify command, or None when this is not a folder of runs.

A single run, including one whose evidence is missing or tampered, returns
None so the existing verifier and fleet exit codes stay in place.
"""
classified = classify_workspace_root(root)
if classified.get("root_kind") != "collection":
return None
runs = classified.get("runs")
if not isinstance(runs, list):
return None
run_ids = [run_id for run_id in runs if isinstance(run_id, str) and run_id]
if not run_ids:
return None
verify_command = "torq evidence verify --run-root " + shlex.quote(str(root / run_ids[0]))
return run_ids, verify_command


def _emit_run_collection_notice(root: Path) -> int | None:
found = _run_collection(root)
if found is None:
return None
run_ids, verify_command = found
listed = "\n".join(f" {run_id}" for run_id in run_ids)
notice = (
"This is a folder of runs.\n"
"Run ids:\n"
f"{listed}\n"
"Verify one run:\n"
f"{verify_command}\n"
)
# Scripts read one JSON object on stdout. The same facts go to stderr.
print(
json.dumps(
{
"status": "run_folder",
"run_ids": run_ids,
"verify_command": verify_command,
}
),
flush=True,
)
sys.stderr.write(notice)
sys.stderr.flush()
# Usage mistake: exit 2 (invalid). Single-run failures keep their own codes.
return 2


def main(argv: Sequence[str] | None = None) -> int:
supplied = list(argv) if argv is not None else sys.argv[1:]
import_boundary = (
Expand Down Expand Up @@ -595,6 +645,10 @@ def chat_snapshot_provider() -> Mapping[str, Any]:
if task_runtime_root is not None:
shutil.rmtree(task_runtime_root, ignore_errors=True)
return 0
if args.run_root is not None:
collection_code = _emit_run_collection_notice(selected_run_root)
if collection_code is not None:
return collection_code
snapshot = projector.snapshot()
print(json.dumps(snapshot, sort_keys=True))
return {
Expand All @@ -606,6 +660,9 @@ def chat_snapshot_provider() -> Mapping[str, Any]:
"unreadable": 4,
}[str(snapshot["verification"]["state"])]
if args.command == "evidence":
collection_code = _emit_run_collection_notice(Path(args.run_root))
if collection_code is not None:
return collection_code
result = verify_receipt_store(Path(args.run_root), trusted_public_key=trusted_public_key)
print(json.dumps({"status": result.status, "finding": result.finding}, sort_keys=True))
return result.exit_code
Expand Down
Loading
Loading