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
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ allstack -C /path/to/repository spec approve docs/plans/change-design.md --by "A
allstack -C /path/to/repository spec check docs/plans/change-design.md
```

- `review` starts a read-only host process (`codex exec --sandbox read-only` or `claude --print` with Read, Glob, and Grep only) with a 240-second deadline. Its prompt contains the draft path and repository root. The receipt is written under `.allstack/spec/reviews/<digest>-<uuid>.json`, and the JSON output includes its `receipt` path and `verdict` (`clean`, `findings`, or `unknown`). A failed host exits nonzero and writes no receipt.
- `review` starts a read-only host process (`codex exec --sandbox read-only` or `claude --print` with Read, Glob, and Grep only) with a 280-second deadline. Its prompt contains the draft path and repository root and limits the reviewer to 12 code locations. The receipt is written under `.allstack/spec/reviews/<digest>-<uuid>.json`, and the JSON output includes its `receipt` path and `verdict` (`clean`, `findings`, or `unknown`). A failed host exits nonzero, writes no receipt, and names its exit state and process evidence directory in the error.
- `approve` records the file's SHA-256 digest, the approver, and the matching receipt path. It refuses when no receipt exists for the current digest. `--without-review` records an explicit override with no receipt. Approval requires a receipt whose verdict is `clean`; `findings` and `unknown` receipts are refused. `--without-review` is the only override. Approval history per design holds at most 1000 events; a further approval fails with `incomplete or oversized fact history`.
- `check` exits 0 only when the latest approval's digest equals the current digest and its receipt, if one was cited, still exists, names that digest, and has verdict `clean`. Otherwise it exits nonzero with the reason: `unapproved`, `changed since approval`, or `unreviewed`. `check` takes the run coordinator lock and fails with `run coordinator busy` during a managed run. When `.allstack` exists it may create store files in it.

Expand Down
2 changes: 1 addition & 1 deletion src/application/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// The reviewer receives the draft path and repository root only, never the author's conversation.
pub fn review_prompt(path: &str, root: &str) -> String {
format!(
"You are an independent design reviewer with no prior conversation. Review the design document at `{path}` in the repository at `{root}`. Read that document and the repository code it names; do not read anything it does not point to. Do not edit files or run commands that change state. Report each defect as: severity, location, consequence, and evidence. End with exactly one line: `VERDICT: clean` when no defect remains, otherwise `VERDICT: findings`."
"You are an independent design reviewer with no prior conversation. Review the design document at `{path}` in the repository at `{root}`. Read that document and the repository code it names; do not read anything it does not point to. The review has a fixed deadline and output appears only when you finish: check at most 12 code locations, starting with the claims the design depends on most, then write the report and name any claim you left unchecked. Do not edit files or run commands that change state. Report each defect as: severity, location, consequence, and evidence. End with exactly one line: `VERDICT: clean` when no defect remains, otherwise `VERDICT: findings`."
)
}

Expand Down
3 changes: 3 additions & 0 deletions src/infrastructure/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub struct Outcome {
pub elapsed_ms: u128,
pub stdout: String,
pub stderr: String,
#[serde(default)]
pub evidence: PathBuf,
}

impl Outcome {
Expand Down Expand Up @@ -187,6 +189,7 @@ pub fn supervise() -> Result<()> {
elapsed_ms: start.elapsed().as_millis(),
stdout: read("stdout")?,
stderr: read("stderr")?,
evidence: spec.directory.clone(),
};
atomic_write(
&spec.directory.join("result.json"),
Expand Down
9 changes: 7 additions & 2 deletions src/infrastructure/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,19 @@ pub async fn review(repo: &Path, path: &str, host: Host) -> Result<Value> {
let outcome = process::run(process::Spec {
argv: hosts::read_only_argv(host),
cwd: repo.into(),
seconds: 240,
// Stays below the 290-second limit on the enclosing operation so a timeout is reported.
seconds: 280,
environment: BTreeMap::new(),
input: Some(rules::review_prompt(path, &repo.to_string_lossy())),
directory: repo.join(".allstack/process"),
})?;
ensure!(
outcome.success(),
"review host failed, timed out, or lost owner; inspect private process evidence: {}",
"review host failed (exit {:?}, timed out {}, owner lost {}); inspect private process evidence at {}: {}",
outcome.code,
outcome.timed_out,
outcome.owner_lost,
outcome.evidence.display(),
outcome.stderr
);
let findings = hosts::final_text(host, &outcome.stdout)?;
Expand Down
11 changes: 11 additions & 0 deletions tests/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ fn review_runs_fresh_host_and_approval_binds_the_digest() {
let prompt = f.captured_prompt();
assert!(prompt.contains(DESIGN), "{prompt}");
assert!(prompt.contains("no prior conversation"), "{prompt}");
assert!(prompt.contains("at most 12 code locations"), "{prompt}");
assert!(prompt.len() < 2000, "reviewer prompt carries extra context");

let approved = f.json(&["spec", "approve", DESIGN, "--by", "reviewer"]);
Expand Down Expand Up @@ -137,6 +138,16 @@ fn failed_host_fails_the_review_and_writes_no_receipt() {
let f = Fixture::new(FAIL);
let (ok, _, err) = f.run(&["spec", "review", DESIGN]);
assert!(!ok && err.contains("review host failed"), "{err}");
assert!(err.contains("exit Some(3)"), "{err}");
let evidence = err
.split("evidence at ")
.nth(1)
.and_then(|rest| rest.split(':').next())
.unwrap_or_else(|| panic!("no evidence path: {err}"));
assert!(
PathBuf::from(evidence).join("result.json").is_file(),
"{err}"
);
assert!(!f.repo.join(".allstack/spec/reviews").exists());
}

Expand Down
Loading