use std::ffi::OsString;
use std::io::Write;
use std::process::ExitCode;
#[cfg(feature = "longitudinal-counting")]
use base64::Engine as _;
#[cfg(feature = "longitudinal-counting")]
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use clap::error::ErrorKind;
use clap::{Parser, Subcommand};
use crate::cli_tracing::TracingArgs;
mod assessment;
mod association;
mod attention;
mod capture;
pub(crate) mod common;
mod derived_read;
mod diff;
mod endorse;
mod history;
mod id_resolver;
mod identity;
mod input_request;
mod inspect;
mod json;
mod key;
mod observation;
mod output;
mod revision;
mod store;
mod theme;
mod validation;
mod version;
#[cfg(test)]
mod about_bleed_guard;
#[cfg(test)]
mod help_hygiene_guard;
#[cfg(test)]
mod help_vocab_guard;
#[cfg(test)]
mod reference_coverage;
#[cfg(test)]
mod workflow_help_guard;
const ROOT_LONG_ABOUT: &str = "\
Durable, local-first review record for code changes that humans and coding \
agents build together.
A review moves through five stages: Work -> Claims -> Evidence -> Questions -> Call.
Work — what changed: capture, revision, inspect
Claims — what an author or reviewer asserts: observation
Evidence — what was checked: validation
Questions — what still needs judgment: input-request
Call — the current assessment: assessment
Across the stages, attention lists the outstanding judgment, and association
records where the reviewed work landed.
First review, from a real tracked change in a Git repository:
pointbreak capture --summary \"<what changed>\"
pointbreak inspect --open
That opens the local, read-only Review. The Getting Started guide continues
with the complete paired author/reviewer loop:
https://github.com/withpointbreak/pointbreak/blob/main/docs/getting-started.md
Recovery:
wrong repository or store pointbreak store paths --repo <repo> --format text
more than one revision pointbreak revision list, then pass --revision <id>
replace an earlier call pointbreak assessment add --replaces <assessment-id>
commit landed after capture pointbreak association record --commit <oid>
(same revision — a landing is never a recapture)";
#[derive(Debug, Parser)]
#[command(
name = "pointbreak",
bin_name = "pointbreak",
version = pointbreak::documents::VERSION_DISPLAY,
about = "Durable, local-first review record for code changes",
long_about = ROOT_LONG_ABOUT
)]
struct Cli {
#[command(flatten)]
tracing: TracingArgs,
#[cfg(feature = "longitudinal-counting")]
#[arg(long, global = true, hide = true)]
longitudinal_counting: Option<String>,
#[command(subcommand)]
command: Command,
}
#[cfg(feature = "longitudinal-counting")]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct LongitudinalCliCountingRequest {
run_identity: String,
context: pointbreak::bench_support::longitudinal::LongitudinalCounterReceiptContextV1,
receipt_path: std::path::PathBuf,
}
#[derive(Debug, Subcommand)]
enum Command {
Assessment(Box<assessment::AssessmentArgs>),
Association(Box<association::AssociationArgs>),
Attention(attention::AttentionArgs),
Capture(capture::CaptureArgs),
Diff(diff::DiffArgs),
Endorse(endorse::EndorseArgs),
History(history::HistoryArgs),
Identity(identity::IdentityArgs),
InputRequest(Box<input_request::InputRequestArgs>),
Inspect(inspect::InspectArgs),
Key(key::KeyArgs),
Observation(Box<observation::ObservationArgs>),
Revision(revision::RevisionArgs),
Store(store::StoreArgs),
Validation(validation::ValidationArgs),
Version(version::VersionArgs),
}
pub(crate) fn run_main() -> ExitCode {
let mut stdout = std::io::stdout().lock();
let mut stderr = std::io::stderr().lock();
run_with_io(std::env::args_os(), &mut stdout, &mut stderr)
}
fn run_with_io<I, S>(args: I, stdout: &mut dyn Write, stderr: &mut dyn Write) -> ExitCode
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{
let args: Vec<OsString> = args.into_iter().map(Into::into).collect();
let invalid_subcommand_hint = invalid_subcommand_hint(&args);
let cli = match Cli::try_parse_from(args) {
Ok(cli) => cli,
Err(error) => {
let exit = if matches!(
error.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) {
let _ = write!(stdout, "{error}");
ExitCode::SUCCESS
} else {
let _ = writeln!(stderr, "{error}");
if error.kind() == ErrorKind::InvalidSubcommand
&& let Some(hint) = invalid_subcommand_hint
{
let _ = writeln!(stderr, "\n{hint}");
}
ExitCode::FAILURE
};
return exit;
}
};
#[cfg(feature = "longitudinal-counting")]
{
let mut cli = cli;
if let Some(encoded) = cli.longitudinal_counting.take() {
return run_counted_cli(cli, stdout, stderr, &encoded);
}
result_exit_code(run_cli(cli, stdout, stderr), stderr)
}
#[cfg(not(feature = "longitudinal-counting"))]
match run_cli(cli, stdout, stderr) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
let _ = writeln!(stderr, "{error}");
ExitCode::FAILURE
}
}
}
#[cfg(feature = "longitudinal-counting")]
fn result_exit_code(
result: Result<(), Box<dyn std::error::Error>>,
stderr: &mut dyn Write,
) -> ExitCode {
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
let _ = writeln!(stderr, "{error}");
ExitCode::FAILURE
}
}
}
#[cfg(feature = "longitudinal-counting")]
fn run_counted_cli(
cli: Cli,
stdout: &mut dyn Write,
stderr: &mut dyn Write,
encoded: &str,
) -> ExitCode {
let request = URL_SAFE_NO_PAD
.decode(encoded)
.map_err(|error| error.to_string())
.and_then(|bytes| {
serde_json::from_slice::<LongitudinalCliCountingRequest>(&bytes)
.map_err(|error| error.to_string())
});
let request = match request {
Ok(request) => request,
Err(error) => {
let _ = writeln!(stderr, "invalid longitudinal counting request: {error}");
return ExitCode::FAILURE;
}
};
if !request.receipt_path.is_absolute() {
let _ = writeln!(
stderr,
"longitudinal counting receipt path must be absolute"
);
return ExitCode::FAILURE;
}
let scope = match pointbreak::bench_support::longitudinal::LongitudinalCountingScopeV1::new(
request.run_identity,
) {
Ok(scope) => scope,
Err(error) => {
let _ = writeln!(stderr, "{error}");
return ExitCode::FAILURE;
}
};
let _guard = scope.enter();
let mut counting_stdout = LongitudinalCountingWriter { inner: stdout };
let result = run_cli(cli, &mut counting_stdout, stderr);
let mut context = request.context;
context.success = result.is_ok();
let receipt = scope
.receipt(context)
.map_err(|error| error.to_string())
.and_then(|receipt| write_counting_receipt(&request.receipt_path, &receipt));
if let Err(error) = receipt {
let _ = writeln!(
stderr,
"could not write longitudinal counting receipt: {error}"
);
return ExitCode::FAILURE;
}
result_exit_code(result, stderr)
}
#[cfg(feature = "longitudinal-counting")]
fn write_counting_receipt(
path: &std::path::Path,
receipt: &pointbreak::bench_support::longitudinal::LongitudinalCounterReceiptV1,
) -> Result<(), String> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|error| error.to_string())?;
serde_json::to_writer(&mut file, receipt).map_err(|error| error.to_string())?;
file.write_all(b"\n").map_err(|error| error.to_string())
}
#[cfg(feature = "longitudinal-counting")]
struct LongitudinalCountingWriter<'a> {
inner: &'a mut dyn Write,
}
#[cfg(feature = "longitudinal-counting")]
impl Write for LongitudinalCountingWriter<'_> {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
let written = self.inner.write(bytes)?;
pointbreak::bench_support::longitudinal::record_response_bytes(written);
Ok(written)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
enum HintPredicate {
LeadingPath(&'static [&'static str]),
AdjacentWindow(&'static [&'static str]),
LeadingToken(&'static str),
}
impl HintPredicate {
fn matches(&self, tokens: &[&str]) -> bool {
match self {
HintPredicate::LeadingPath(path) => tokens
.get(1..)
.is_some_and(|command_args| command_args.starts_with(path)),
HintPredicate::AdjacentWindow(seq) => tokens
.windows(seq.len())
.any(|window| window.iter().zip(seq.iter()).all(|(a, b)| a == b)),
HintPredicate::LeadingToken(name) => tokens
.iter()
.skip(1) .find(|token| !token.starts_with('-'))
.is_some_and(|token| token == name),
}
}
}
const INVALID_SUBCOMMAND_HINTS: &[(HintPredicate, &str)] = &[
(
HintPredicate::LeadingPath(&["assessment", "replace"]),
"Use `pointbreak assessment add --replaces <assessment-id>`.",
),
(
HintPredicate::AdjacentWindow(&["identity", "enroll"]),
"Use `pointbreak identity delegate <AGENT> --principal <P>`.",
),
(
HintPredicate::AdjacentWindow(&["review", "assessment"]),
"Use `pointbreak assessment` instead of `pointbreak review assessment`.",
),
(
HintPredicate::AdjacentWindow(&["review", "association", "associate-commit"]),
"Use `pointbreak association record --commit <oid>` (or `--ref <name> --head <oid>`).",
),
(
HintPredicate::AdjacentWindow(&["review", "association", "associate-ref"]),
"Use `pointbreak association record --ref <name> --head <oid>` (or `--commit <oid>`).",
),
(
HintPredicate::AdjacentWindow(&["review", "association", "withdraw-commit"]),
"Use `pointbreak association withdraw <ASSOCIATION_ID>`.",
),
(
HintPredicate::AdjacentWindow(&["review", "association", "withdraw-ref"]),
"Use `pointbreak association withdraw <ASSOCIATION_ID>`.",
),
(
HintPredicate::AdjacentWindow(&["review", "association"]),
"The `association` family is now top-level; use \
`pointbreak association record|withdraw|list`.",
),
(
HintPredicate::AdjacentWindow(&["review", "capture"]),
"Use `pointbreak capture` instead of `pointbreak review capture`.",
),
(
HintPredicate::AdjacentWindow(&["review", "endorse"]),
"Use `pointbreak endorse` instead of `pointbreak review endorse`.",
),
(
HintPredicate::AdjacentWindow(&["review", "history"]),
"Use `pointbreak history` instead of `pointbreak review history`.",
),
(
HintPredicate::AdjacentWindow(&["review", "input-request", "fetch"]),
"Use `pointbreak input-request show <ID>`.",
),
(
HintPredicate::AdjacentWindow(&["input-request", "fetch"]),
"Use `pointbreak input-request show <ID>`.",
),
(
HintPredicate::AdjacentWindow(&["review", "input-request"]),
"The `input-request` family is now top-level; use \
`pointbreak input-request open|list|show|respond`.",
),
(
HintPredicate::AdjacentWindow(&["review", "observation"]),
"Use `pointbreak observation` instead of `pointbreak review observation`.",
),
(
HintPredicate::AdjacentWindow(&["review", "revisions"]),
"Use `pointbreak revision list`.",
),
(
HintPredicate::AdjacentWindow(&["review", "show"]),
"Use `pointbreak revision show [REVISION]`.",
),
(
HintPredicate::AdjacentWindow(&["review", "validation"]),
"Use `pointbreak validation` instead of `pointbreak review validation`.",
),
(
HintPredicate::AdjacentWindow(&["review", "intervention"]),
"Use `pointbreak input-request` instead of `pointbreak review intervention`.",
),
(
HintPredicate::AdjacentWindow(&["review", "lineage"]),
"`pointbreak review lineage` is removed; record supersession on \
`pointbreak capture --supersedes <revision>` and read it with `pointbreak revision list`.",
),
(
HintPredicate::AdjacentWindow(&["review", "unit"]),
"`pointbreak review unit` is removed; list with `pointbreak revision list` \
and show one with `pointbreak revision show <revision>`.",
),
(
HintPredicate::LeadingToken("review"),
"The `review` family flattened to the top level. Use `pointbreak capture`, \
`pointbreak revision list`, `pointbreak revision show`, `pointbreak observation …`, etc.",
),
(
HintPredicate::LeadingToken("keys"),
"The `keys` family is now `key`. Use `pointbreak key <sub>`.",
),
(
HintPredicate::LeadingToken("dump"),
"`pointbreak dump` is retired. Read a captured revision's diff with `pointbreak diff`, \
inspect deeply with `pointbreak inspect`, or read the review record with \
`pointbreak revision show` (add `--format text` for the digest).",
),
(
HintPredicate::LeadingToken("show"),
"`pointbreak show` is retired. Read a captured revision's diff with `pointbreak diff`, \
inspect deeply with `pointbreak inspect`, or read the review record with \
`pointbreak revision show` (add `--format text` for the digest).",
),
(
HintPredicate::LeadingToken("notes"),
"The `notes` family is retired and sidecar notes are no longer imported. \
Record review facts with `pointbreak observation add` and read them with \
`pointbreak revision show` or `pointbreak inspect`.",
),
];
fn invalid_subcommand_hint(args: &[OsString]) -> Option<&'static str> {
let tokens: Vec<&str> = args.iter().filter_map(|arg| arg.to_str()).collect();
INVALID_SUBCOMMAND_HINTS
.iter()
.find(|(predicate, _)| predicate.matches(&tokens))
.map(|(_, hint)| *hint)
}
fn run_cli(
cli: Cli,
stdout: &mut dyn Write,
stderr: &mut dyn Write,
) -> Result<(), Box<dyn std::error::Error>> {
pointbreak::git::validate_backend_selector()?;
crate::cli_tracing::init_tracing(&cli.tracing)?;
let result = match cli.command {
Command::Assessment(args) => assessment::run(*args, stdout, stderr),
Command::Association(args) => association::run(*args, stdout, stderr),
Command::Attention(args) => attention::run(args, stdout),
Command::Capture(args) => capture::run(args, &cli.tracing, stdout, stderr),
Command::Diff(args) => diff::run(args, stdout),
Command::Endorse(args) => endorse::run(args, stdout, stderr),
Command::History(args) => history::run(args, stdout),
Command::Identity(args) => identity::run(args, stdout, stderr),
Command::InputRequest(args) => input_request::run(*args, stdout, stderr),
Command::Inspect(args) => inspect::run(args, stdout),
Command::Key(args) => key::run(args, stdout),
Command::Observation(args) => observation::run(*args, stdout, stderr),
Command::Revision(args) => revision::run(args, stdout),
Command::Store(args) => store::run(args, stdout, stderr),
Command::Validation(args) => validation::run(args, stdout, stderr),
Command::Version(args) => version::run(args, stdout),
};
for diagnostic in pointbreak::session::take_derived_write_diagnostics() {
let _ = writeln!(stderr, "advisory: {}", diagnostic.message);
}
result
}
#[cfg(all(test, feature = "longitudinal-counting"))]
mod longitudinal_counting_tests {
use super::*;
#[test]
fn final_cli_writer_counts_only_successfully_emitted_stdout_bytes() {
let scope = pointbreak::bench_support::longitudinal::LongitudinalCountingScopeV1::new(
"9".repeat(64),
)
.expect("valid scope");
let _guard = scope.enter();
let mut output = Vec::new();
let mut writer = LongitudinalCountingWriter { inner: &mut output };
writer.write_all(b"pointbreak").expect("write output");
writer.flush().expect("flush output");
assert_eq!(output, b"pointbreak");
assert_eq!(
scope.snapshot().counters.response_bytes,
b"pointbreak".len() as u64
);
}
#[test]
fn hidden_cli_transport_writes_a_disjoint_receipt_after_the_final_stdout_boundary() {
let directory = tempfile::tempdir().expect("temporary receipt directory");
let receipt_path = directory.path().join("receipt.json");
let request = serde_json::json!({
"runIdentity": "1".repeat(64),
"context": {
"rootIdentity": "2".repeat(64),
"operation": "VERSION",
"phase": "cold",
"baseExecutionIdentitySha256": "3".repeat(64),
"derivativeExecutionIdentitySha256": "4".repeat(64),
"manifestSha256": "5".repeat(64),
"scheduleSha256": "6".repeat(64),
"success": false,
"semanticResultSha256": "7".repeat(64),
"includeCapacityOwnership": false
},
"receiptPath": receipt_path
});
let encoded = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&request).expect("request JSON"));
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let exit = run_with_io(
[
"pointbreak",
"--longitudinal-counting",
encoded.as_str(),
"version",
"--format",
"json",
],
&mut stdout,
&mut stderr,
);
assert_eq!(
exit,
ExitCode::SUCCESS,
"{}",
String::from_utf8_lossy(&stderr)
);
let receipt: pointbreak::bench_support::longitudinal::LongitudinalCounterReceiptV1 =
serde_json::from_slice(&std::fs::read(receipt_path).expect("receipt bytes"))
.expect("receipt JSON");
assert!(receipt.success);
assert_eq!(receipt.operation, "VERSION");
assert_eq!(receipt.counters.response_bytes, stdout.len() as u64);
assert!(receipt.capacity_ownership.is_none());
}
}