From a5c6f214cf1de0ef922da831b3a59a00446a9e7b Mon Sep 17 00:00:00 2001 From: Ahmet Taspinar Date: Fri, 2 Oct 2026 21:10:22 +0200 Subject: [PATCH 1/2] Add revise-planning.sh with a recorded decision per finding revise-planning.sh lets the project planner handle a current planning review in two phases: read-only, it decides ADOPT, REJECT, DEFER, or ESCALATE per finding with a rationale, validated like review results; after approval, the decisions are recorded and a write session resolves exactly the adopted findings, limited to the architecture, roadmap, and ADRs. Critical and major findings may only be adopted or escalated. Scope snapshots move from start-planning.sh into scripts/lib/scope.sh, and the read-only runner accepts a validator for other result types. Closes #46 Co-Authored-By: Claude Opus 5.5 --- .agents/prompts/planning-reviser.md | 50 +++++ .agents/schemas/revision.schema.json | 23 +++ docs/development.md | 37 +++- scripts/lib/review-data.sh | 100 +++++++++ scripts/lib/review-run.sh | 14 +- scripts/lib/scope.sh | 79 ++++++++ scripts/review-planning.sh | 8 +- scripts/revise-planning.sh | 290 +++++++++++++++++++++++++++ scripts/start-planning.sh | 69 ++----- scripts/triage-review.sh | 2 +- scripts/verify.conf | 1 + tests/lib-fakes.sh | 16 +- tests/revise-planning-test.sh | 227 +++++++++++++++++++++ tests/start-planning-test.sh | 2 +- 14 files changed, 845 insertions(+), 73 deletions(-) create mode 100644 .agents/prompts/planning-reviser.md create mode 100644 .agents/schemas/revision.schema.json create mode 100644 scripts/lib/scope.sh create mode 100755 scripts/revise-planning.sh create mode 100755 tests/revise-planning-test.sh diff --git a/.agents/prompts/planning-reviser.md b/.agents/prompts/planning-reviser.md new file mode 100644 index 0000000..fdd9004 --- /dev/null +++ b/.agents/prompts/planning-reviser.md @@ -0,0 +1,50 @@ +# Planning Revision Contract + +You are the project planner. An independent planning review assessed your +architecture, roadmap, and ADRs. `revise-planning.sh` runs you in two phases +in the planning worktree. + +Read `AGENTS.md`, `.agents/prompts/project-planner.md`, the approved +`docs/PROJECT_REQUIREMENTS.md`, `docs/PROJECT_DESCRIPTION.md` when present, +the current planning documents, and the review supplied by the caller. + +The approved requirements are authoritative. You may not change them, and you +may not resolve a finding by silently departing from them. + +## Phase 1: decide + +You run read-only. Decide exactly once for every finding of the review, using +its `id` as `finding_id`: + +- `ADOPT`: the finding is valid and you will change the architecture, roadmap, + or ADRs to resolve it. +- `REJECT`: the finding is wrong or the current planning is the better choice. + Explain why in terms of the requirements. +- `DEFER`: the finding is valid, but belongs to a later roadmap feature or a + just-in-time feature plan, not to the project planning. +- `ESCALATE`: resolving the finding needs a human product decision or a change + to the approved requirements. + +Critical and major findings may only be adopted or escalated. Every decision +needs a rationale. + +Return JSON that matches the schema supplied by the caller +(`.agents/schemas/revision.schema.json`). Do not write files and do not add +text around it. + +## Phase 2: revise + +You run with write access, after the human approved your decisions. Resolve +exactly the adopted findings listed by the caller: + +- change only `docs/architecture.md`, `docs/roadmap.md`, and direct Markdown + ADRs under `docs/decisions/`; +- keep roadmap feature IDs stable; add new IDs instead of renumbering; +- keep the documents consistent with each other and with the requirements; +- do not address rejected, deferred, or escalated findings; +- do not modify the requirements, the project description, review or revision + artifacts, or any other file; +- do not commit, push, open or merge a PR, create Issues, or deploy. + +Finish when every adopted finding is resolved. A new planning review round +confirms the result. diff --git a/.agents/schemas/revision.schema.json b/.agents/schemas/revision.schema.json new file mode 100644 index 0000000..75df0c1 --- /dev/null +++ b/.agents/schemas/revision.schema.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "additionalProperties": false, + "required": ["decisions"], + "properties": { + "decisions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["finding_id", "decision", "rationale"], + "properties": { + "finding_id": { "type": "string" }, + "decision": { + "type": "string", + "enum": ["ADOPT", "REJECT", "DEFER", "ESCALATE"] + }, + "rationale": { "type": "string" } + } + } + } + } +} diff --git a/docs/development.md b/docs/development.md index 197d18a..ab414a1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -182,8 +182,41 @@ Each round is stored as `.agents/reviews/planning--review-NN.json` with a generated report, in the same format as feature reviews but without an Issue. The review covers only the planning documents, so it becomes stale when one of them is added, changed, or deleted, and stays current otherwise -(`./scripts/check-review.sh`). A planning review is not triaged: revise the -planning documents for the findings you accept and run the review again. +(`./scripts/check-review.sh`). A planning review is not triaged; it is +revised. + +### Planning revision + +Let the original project planner handle the findings of a current planning +review: + +```bash +./scripts/revise-planning.sh --review .agents/reviews/planning-project-bootstrap-review-01.json +``` + +It uses role `project-planner` in two phases: + +1. **Decide.** Read-only, the planner returns one decision per finding with a + rationale: `ADOPT`, `REJECT`, `DEFER` (belongs to a later feature), or + `ESCALATE` (needs a human product decision or a change to the approved + requirements). Critical and major findings may only be adopted or + escalated. The decisions are validated like review results, shown to you, + and recorded only after your approval, as + `.agents/reviews/-revision.json` with a generated report. +2. **Revise.** A write session resolves exactly the adopted findings. It may + change only `docs/architecture.md`, `docs/roadmap.md`, and direct Markdown + ADRs. Any other change, including to the requirements, the description, or + a review artifact, and any commit fails the run and keeps the worktree for + inspection. + +Escalated findings are listed with the next step: change the requirements +through Project Grill and approve them again, or decide that the finding does +not apply. When the write session fails, the approved decisions stay recorded; +while the review is still current, running the script again applies them +without deciding again. + +After a revision the review is stale by design. Run `review-planning.sh` for +the next round, and repeat until the review passes. Open and merge a planning PR before creating Issues for actionable roadmap features. The script never implements features, creates Issues, commits, diff --git a/scripts/lib/review-data.sh b/scripts/lib/review-data.sh index b2f4ce6..48dc0ae 100644 --- a/scripts/lib/review-data.sh +++ b/scripts/lib/review-data.sh @@ -119,6 +119,36 @@ _review_data_rules=' else empty end) end) end; + + # Input: {decisions}. Revision decisions of the project planner about the + # findings of a planning review. + def revision_decision_rules($severity): + if type != "object" or (.decisions | type) != "array" then + "the result must be an object with a decisions array" + else + unexpected(["decisions"]; "the result"), + ([.decisions[] | objects | .finding_id] as $decided + | ($severity | keys[]) as $id + | ($decided | map(select(. == $id)) | length) as $count + | if $count == 0 then "finding \($id) has no revision decision" + elif $count > 1 then "finding \($id) has more than one revision decision" + else empty end), + (.decisions | to_entries[] | (.key + 1) as $n | .value as $d | + if ($d | type) != "object" then + "decision \($n) is not an object" + elif ($d.finding_id | type) != "string" or ($severity | has($d.finding_id) | not) then + "decision \($n) refers to an unknown finding: \($d.finding_id | tostring)" + else + ($d | unexpected(["decision", "finding_id", "rationale"]; "decision \($n)")), + (if ($d.decision | IN("ADOPT", "REJECT", "DEFER", "ESCALATE")) then empty + else "finding \($d.finding_id) has an invalid decision" end), + (if ($d.rationale | nonempty) then empty + else "finding \($d.finding_id) has no rationale" end), + (if ($severity[$d.finding_id] | IN("critical", "major")) and ($d.decision | IN("REJECT", "DEFER")) then + "\($severity[$d.finding_id]) finding \($d.finding_id) must be adopted or escalated" + else empty end) + end) + end; ' # review_result_errors : rules for a reviewer's result. @@ -320,6 +350,76 @@ triage_artifact_errors() { ' "$1" } +# revision_decision_errors : rules for the +# project planner's decisions about the findings of a planning review. +revision_decision_errors() { + review_data_is_json "$1" || { + echo "the decisions are not exactly one JSON value" + return 0 + } + + jq -r --slurpfile review "$2" "$_review_data_rules"' + revision_decision_rules($review[0].findings | map({key: .id, value: .severity}) | from_entries) + ' "$1" +} + +# revision_artifact_errors : rules for a stored +# revision, checked against its planning review. +revision_artifact_errors() { + review_data_is_json "$1" || { + echo "the revision is not exactly one JSON value" + return 0 + } + + jq -r --slurpfile review "$2" "$_review_data_rules"' + if type != "object" or .schema != "revision/v1" then + "the file is not a revision/v1 artifact" + else + unexpected(["approved_at", "branch", "decisions", "planner", "review_round", "reviewed_tree", + "schema", "source_review"]; "the revision"), + (if (.approved_at | type) == "string" + and (.approved_at | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")) then empty + else "the revision has no valid UTC approval timestamp" end), + (if .branch == $review[0].branch and .review_round == $review[0].round + and .reviewed_tree == $review[0].reviewed_tree then empty + else "the revision does not describe its planning review" end), + (if (.planner | type) == "object" and (.planner.agent | nonempty) and (.planner.model | nonempty) then empty + else "the revision must name its planner agent and model" end), + ($review[0].findings | map({key: .id, value: {severity, title}}) | from_entries) as $source + | (if (.decisions | type) == "array" then + (.decisions[] | objects | + unexpected(["decision", "finding_id", "rationale", "severity", "title"]; "the decision for \(.finding_id | tostring)"), + (if $source[.finding_id] == null or {severity, title} == $source[.finding_id] then empty + else "the decision for \(.finding_id) does not match the severity and title in the review" end)) + else empty end), + ({decisions: (if (.decisions | type) == "array" + then (.decisions | map(if type == "object" then {finding_id, decision, rationale} else . end)) + else .decisions end)} + | revision_decision_rules($review[0].findings | map({key: .id, value: .severity}) | from_entries)) + end + ' "$1" +} + +# revision_render_markdown +revision_render_markdown() { + jq -r --arg source "$2" ' + def group($decision; $heading): + "## \($heading)\n\n" + + ([.decisions[] | select(.decision == $decision)] as $items + | if ($items | length) == 0 then "None.\n" + else ($items | map("### \(.finding_id). \(.title)\n\n- Severity: \(.severity)\n- Rationale: \(.rationale)\n") | join("\n")) end); + "\n\n" + + "# Planning Revision — \(.branch), review round \(.review_round)\n\n" + + "Source review: `\(.source_review)`\n\n" + + "Planner: \(.planner.agent) (\(.planner.model))\n\n" + + "Approved at: \(.approved_at)\n\n" + + group("ADOPT"; "Adopted") + "\n" + + group("REJECT"; "Rejected") + "\n" + + group("DEFER"; "Deferred") + "\n" + + group("ESCALATE"; "Escalated to the human") + ' "$1" +} + # triage_render_markdown triage_render_markdown() { jq -r --arg source "$2" ' diff --git a/scripts/lib/review-run.sh b/scripts/lib/review-run.sh index 5d7b80e..bdfec0a 100644 --- a/scripts/lib/review-run.sh +++ b/scripts/lib/review-run.sh @@ -4,11 +4,12 @@ # review-feature.sh and review-planning.sh. # Source this file after agent.sh, review-data.sh, and fingerprint.sh. -# review_run_reviewer -# Runs the reviewer read-only. An invalid result is retried once, with the -# reasons for the rejection. Exits the calling script when the reviewer fails, -# changes the working tree or the review artifacts, creates a commit, or -# returns an invalid result twice. +# review_run_reviewer [validator] +# Runs a read-only agent that returns structured output. The validator prints +# the rule violations of a result file and defaults to review_result_errors. +# An invalid result is retried once, with the reasons for the rejection. Exits +# the calling script when the agent fails, changes the working tree or the +# review artifacts, creates a commit, or returns an invalid result twice. review_run_reviewer() { local agent="$1" local model="$2" @@ -18,6 +19,7 @@ review_run_reviewer() { local schema_file="$6" local result_file="$7" local scratch="$8/review-run" + local validator="${9:-review_result_errors}" local head_before local tree_before local artifacts_before @@ -53,7 +55,7 @@ review_run_reviewer() { exit 1 fi - result_errors="$(review_result_errors "$result_file")" + result_errors="$("$validator" "$result_file")" [[ -n "$result_errors" ]] || return 0 echo "The reviewer returned an invalid result (attempt $attempt of 2):" >&2 diff --git a/scripts/lib/scope.sh b/scripts/lib/scope.sh new file mode 100644 index 0000000..ce824a3 --- /dev/null +++ b/scripts/lib/scope.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# Filesystem scope enforcement for write-capable agent sessions. +# Source this file; do not execute it. +# +# A snapshot records every path in a worktree, including untracked and ignored +# files but not .git, with its type, mode, and content hash. Paths that the +# session may change are left out by a caller-supplied function. Comparing a +# snapshot taken before and after the session shows every out-of-scope change. + +scope_file_mode() { + if [[ "$(uname -s)" == "Darwin" ]]; then + stat -f '%Lp' "$1" + else + stat -c '%a' "$1" + fi +} + +# Allowed paths of the project planner: the architecture, the roadmap, and +# direct Markdown ADRs. +scope_planner_allowed() { + case "$1" in + docs/architecture.md | docs/roadmap.md) + return 0 + ;; + docs/decisions/*.md) + [[ "${1#docs/decisions/}" != */* ]] + return + ;; + *) + return 1 + ;; + esac +} + +# scope_snapshot +# Paths are read NUL-delimited and written escaped, so names with tabs or +# newlines cannot spoof an allowed path. +scope_snapshot() { + local worktree="$1" + local allowed="$2" + local target="$3" + + ( + cd "$worktree" + find . -mindepth 1 ! -path './.git' -print0 | + while IFS= read -r -d '' relative; do + path="${relative#./}" + if "$allowed" "$path"; then + continue + fi + + if [[ -L "$path" ]]; then + signature="symlink:$(scope_file_mode "$path"):$(readlink "$path")" + elif [[ -f "$path" ]]; then + signature="regular:$(scope_file_mode "$path"):$(git hash-object -- "$path")" + elif [[ -d "$path" ]]; then + signature="directory:$(scope_file_mode "$path")" + else + signature="other:$(scope_file_mode "$path")" + fi + + path_key="$(printf '%s' "$path" | git hash-object --stdin)" + printf '%s\t%q\t%s\n' "$path_key" "$path" "$signature" + done + ) | LC_ALL=C sort >"$target" +} + +# scope_unchanged +# Succeeds when both snapshots are equal; otherwise prints the escaped +# differences to standard error. +scope_unchanged() { + if cmp -s "$1" "$2"; then + return 0 + fi + echo "Detected out-of-scope filesystem changes (escaped paths shown):" >&2 + diff -u "$1" "$2" >&2 || true + return 1 +} diff --git a/scripts/review-planning.sh b/scripts/review-planning.sh index 9517edf..f75e151 100755 --- a/scripts/review-planning.sh +++ b/scripts/review-planning.sh @@ -177,6 +177,8 @@ review_store "$result_file" "$out" "$metadata" echo " $review_relative.json (source of truth)" echo " $review_relative.md (generated report)" echo -echo "Next: revise the planning documents in this worktree for the findings you" -echo "accept, then run ./scripts/review-planning.sh again. When the review passes," -echo "commit the planning and open the planning PR as described in docs/development.md." +if [[ "$(jq '.findings | length' "$out.json")" -gt 0 ]]; then + echo "Next: ./scripts/revise-planning.sh --review $review_relative.json" +else + echo "Next: commit the planning and open the planning PR as described in docs/development.md." +fi diff --git a/scripts/revise-planning.sh b/scripts/revise-planning.sh new file mode 100755 index 0000000..7bfe2fe --- /dev/null +++ b/scripts/revise-planning.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +source "$script_dir/lib/agent.sh" +source "$script_dir/lib/review-data.sh" +source "$script_dir/lib/fingerprint.sh" +source "$script_dir/lib/review-run.sh" +source "$script_dir/lib/scope.sh" + +usage() { + echo "Usage: $0 --review [--agent ] [--model ]" + echo + echo "Run in the planning worktree. The project planner (role 'project-planner')" + echo "first decides per finding, then, after your approval, revises the planning" + echo "documents for the adopted findings." + exit 1 +} + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +agent_parse_args "$@" +review_input="" +set -- ${AGENT_POSITIONAL[@]+"${AGENT_POSITIONAL[@]}"} +while [[ $# -gt 0 ]]; do + case "$1" in + --review) + [[ $# -ge 2 && -n "$2" ]] || fail "--review requires a planning review file." + review_input="$2" + shift 2 + ;; + *) + usage + ;; + esac +done +[[ -n "$review_input" ]] || usage + +root="$(git rev-parse --show-toplevel)" +branch="$(git branch --show-current)" +[[ "$branch" == planning/* ]] || + fail "revise-planning.sh must run in a planning worktree (branch planning/). Current branch: $branch" + +prompt_file="$root/.agents/prompts/planning-reviser.md" +schema_file="$root/.agents/schemas/revision.schema.json" +[[ -f "$prompt_file" ]] || fail "planning revision prompt not found: $prompt_file" +[[ -f "$schema_file" ]] || fail "revision schema not found: $schema_file" +review_data_require_jq + +[[ -f "$review_input" ]] || fail "planning review not found: $review_input" +review_path="$(cd "$(dirname "$review_input")" && pwd -P)/$(basename "$review_input")" +[[ "$review_path" == "$root/.agents/reviews/"*.json && "$review_path" != *-revision.json ]] || + fail "the review must match .agents/reviews/*.json. Received: $review_path" +review_relative="${review_path#"$root"/}" + +review_errors="$(review_artifact_errors "$review_path")" +if [[ -n "$review_errors" ]]; then + echo "Error: invalid planning review:" >&2 + printf '%s\n' "$review_errors" | sed 's/^/ - /' >&2 + exit 1 +fi +[[ "$(jq -r '.kind // "feature"' "$review_path")" == "planning" ]] || + fail "this is a feature review; use triage-review.sh." +[[ "$(jq -r '.branch' "$review_path")" == "$branch" ]] || + fail "the review belongs to $(jq -r '.branch' "$review_path"), not to $branch." +grep -Fqx "Status: Approved" "$root/docs/PROJECT_REQUIREMENTS.md" 2>/dev/null || + fail "the project requirements are not approved." + +agent_resolve "$root" project-planner "$AGENT_CLI_PROVIDER" "$AGENT_CLI_MODEL" +agent="$AGENT_PROVIDER" +model="$AGENT_MODEL" + +tmp_work="$(mktemp -d "${TMPDIR:-/tmp}/revise-planning.XXXXXX")" +trap 'rm -rf "$tmp_work"' EXIT + +revision="${review_path%.json}-revision" +revision_relative="${revision#"$root"/}" + +current=0 +review_is_current "$root" "$tmp_work" "$review_path" || current=$? +case "$current" in + 0) ;; + 1) + if [[ -f "$revision.json" ]]; then + fail "this review was already revised and the planning has changed since. Run ./scripts/review-planning.sh for the next round." + fi + fail "the planning review is stale: a planning document changed after it was written. Run ./scripts/review-planning.sh again." + ;; + *) + fail "could not compute the fingerprint of the planning documents." + ;; +esac + +if [[ "$(jq '.findings | length' "$review_path")" -eq 0 ]]; then + echo "The planning review has no findings; there is nothing to revise." + exit 0 +fi + +render_decisions() { + jq -r ' + def group($decision; $label): + $label, + ([.[] | select(.decision == $decision)] as $items + | if ($items | length) == 0 then "- (none)" + else ($items[] | "- \(.finding_id) [\(.severity)] \(.title) — \(.rationale)") end), + ""; + group("ADOPT"; "ADOPT"), group("REJECT"; "REJECT"), group("DEFER"; "DEFER"), group("ESCALATE"; "ESCALATE") + ' "$1" +} + +if [[ -f "$revision.json" ]]; then + # Decisions were approved earlier for this unchanged review; only the + # adopted findings remain to be applied. + revision_errors="$(revision_artifact_errors "$revision.json" "$review_path")" + if [[ -n "$revision_errors" ]]; then + echo "Error: invalid revision artifact $revision_relative.json:" >&2 + printf '%s\n' "$revision_errors" | sed 's/^/ - /' >&2 + exit 1 + fi + echo "Using the approved decisions in $revision_relative.json." +else + context_file="$tmp_work/context.md" + decisions_file="$tmp_work/decisions.json" + { + echo "# Planning review to decide on" + echo + echo "Source: $review_relative (round $(jq -r '.round' "$review_path"))" + echo + jq '{verdict, limitations, findings}' "$review_path" + } >"$context_file" + + decide_prompt="Read and follow .agents/prompts/planning-reviser.md, phase 1 (decide). + +Standard input contains the findings of the planning review ${review_relative}. +Decide exactly once per finding and return JSON that matches the supplied +schema. You have read-only access; do not modify any file." + + validate_decisions() { + revision_decision_errors "$1" "$review_path" + } + + echo "Starting $agent planner ($model) to decide per finding, read-only..." + echo + review_run_reviewer "$agent" "$model" "$root" "$decide_prompt" "$context_file" "$schema_file" \ + "$decisions_file" "$tmp_work" validate_decisions + + jq -n --slurpfile review "$review_path" --slurpfile result "$decisions_file" ' + ($result[0].decisions | map({key: .finding_id, value: .}) | from_entries) as $decision + | [$review[0].findings[] | { + finding_id: .id, + severity: .severity, + title: .title, + decision: $decision[.id].decision, + rationale: $decision[.id].rationale + }] + ' >"$tmp_work/proposal.json" + + echo + echo "Proposed revision decisions:" + echo + render_decisions "$tmp_work/proposal.json" + printf "Record these decisions and revise the planning for the adopted findings? [y/N] " + approval="" + read -r approval || true + case "$approval" in + y | Y | yes | YES) ;; + *) + echo "Declined; no decisions were recorded and no document was changed." + exit 0 + ;; + esac + + jq -n \ + --slurpfile review "$review_path" \ + --slurpfile decisions "$tmp_work/proposal.json" \ + --arg source_review "$review_relative" \ + --arg agent "$agent" \ + --arg model "$model" \ + --arg approved_at "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" ' + { + schema: "revision/v1", + source_review: $source_review, + branch: $review[0].branch, + review_round: $review[0].round, + reviewed_tree: $review[0].reviewed_tree, + planner: {agent: $agent, model: $model}, + approved_at: $approved_at, + decisions: $decisions[0] + }' >"$tmp_work/revision.json" + + revision_errors="$(revision_artifact_errors "$tmp_work/revision.json" "$review_path")" + if [[ -n "$revision_errors" ]]; then + echo "Error: the revision would be invalid; nothing was recorded:" >&2 + printf '%s\n' "$revision_errors" | sed 's/^/ - /' >&2 + exit 1 + fi + cp "$tmp_work/revision.json" "$revision.json" + revision_render_markdown "$revision.json" "$(basename "$revision").json" >"$revision.md" + echo + echo "Recorded the decisions:" + echo " $revision_relative.json (source of truth)" + echo " $revision_relative.md (generated report)" +fi + +escalated="$(jq -r '.decisions[] | select(.decision == "ESCALATE") | "- \(.finding_id) \(.title)"' "$revision.json")" +if [[ -n "$escalated" ]]; then + echo + echo "Escalated findings need your decision:" + printf '%s\n' "$escalated" + echo "Resolve each by changing the requirements through Project Grill and approving" + echo "them again, or by deciding that the finding does not apply. The planning" + echo "cannot be finished while an escalated finding is unresolved." +fi + +adopted="$(jq -r --slurpfile review "$review_path" ' + .decisions[] | select(.decision == "ADOPT") | . as $d + | ($review[0].findings[] | select(.id == $d.finding_id)) as $f + | "### \($f.id). \($f.title)\n\n- Severity: \($f.severity)\n- Evidence: \($f.evidence)\n- Impact: \($f.impact)\n- Recommended action: \($f.recommendation)\n- Your rationale: \($d.rationale)\n" +' "$revision.json")" +if [[ -z "$adopted" ]]; then + echo + echo "No finding was adopted; no planning document is changed." + exit 0 +fi + +# Phase 2: the planner may change only the architecture, the roadmap, and +# direct Markdown ADRs. Everything else, including the requirements, the +# description, and all review artifacts, must stay unchanged. +baseline="$tmp_work/scope-before.tsv" +scope_snapshot "$root" scope_planner_allowed "$baseline" +head_before="$(git -C "$root" rev-parse HEAD)" + +revise_prompt="Read and follow .agents/prompts/planning-reviser.md, phase 2 (revise). + +The human approved your decisions in ${revision_relative}.json. Resolve exactly +these adopted findings of ${review_relative}: + +${adopted} +Change only docs/architecture.md, docs/roadmap.md, and direct Markdown ADRs +under docs/decisions/. Do not commit, push, open or merge a pull request, +create GitHub Issues, or deploy." + +echo +echo "Starting $agent planner ($model) to revise the adopted findings..." +echo +agent_status=0 +agent_run write "$agent" "$model" "$root" "$revise_prompt" || agent_status=$? + +preserved() { + echo "Error: $1" >&2 + echo "The planning worktree is preserved; inspect it with: git -C \"$root\" status --short" >&2 + exit "${2:-1}" +} + +[[ "$(git -C "$root" rev-parse HEAD)" == "$head_before" ]] || + preserved "the planner created a commit; planning agents must leave HEAD unchanged." +scope_snapshot "$root" scope_planner_allowed "$tmp_work/scope-after.tsv" +scope_unchanged "$baseline" "$tmp_work/scope-after.tsv" || + preserved "the planner changed files outside the architecture, the roadmap, and the ADRs." +[[ "$agent_status" -eq 0 ]] || preserved "the planner failed with status $agent_status." "$agent_status" + +for document in docs/architecture.md docs/roadmap.md; do + [[ -f "$root/$document" && ! -L "$root/$document" && -s "$root/$document" ]] || + preserved "$document must remain a non-empty regular file." + [[ ! "$(scope_file_mode "$root/$document")" =~ [1357] ]] || + preserved "$document must not be executable." +done +if [[ -e "$root/docs/decisions" || -L "$root/docs/decisions" ]]; then + [[ -d "$root/docs/decisions" && ! -L "$root/docs/decisions" ]] || + preserved "docs/decisions must be a regular directory." + while IFS= read -r -d '' decision; do + name="${decision#"$root/docs/decisions/"}" + [[ "$name" == *.md && -f "$decision" && ! -L "$decision" ]] || + preserved "decision records must be regular Markdown files: docs/decisions/$name" + done < <(find "$root/docs/decisions" -mindepth 1 -maxdepth 1 -print0) +fi + +# Adopted findings must lead to a change of the planning documents. +after_revision="$(fingerprint_review "$root" "$tmp_work" "$review_path")" || + preserved "could not compute the fingerprint of the planning documents." +[[ "$after_revision" != "$(jq -r '.reviewed_tree' "$review_path")" ]] || + preserved "the planner changed no planning document for the adopted findings. The approved decisions are kept; run this script again to apply them." + +echo +echo "The adopted findings were revised. The planning review is now stale by design." +echo "Next: run ./scripts/review-planning.sh for the next review round." diff --git a/scripts/start-planning.sh b/scripts/start-planning.sh index 1a16788..9cc136e 100755 --- a/scripts/start-planning.sh +++ b/scripts/start-planning.sh @@ -3,6 +3,7 @@ set -euo pipefail source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/lib/agent.sh" +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/lib/scope.sh" usage() { cat <"$target" + scope_snapshot "$worktree" "${1}_allowed" "$2" } require_scope_unchanged() { @@ -239,11 +197,8 @@ require_scope_unchanged() { local current_snapshot="$state_dir/${scope}-current.tsv" snapshot_forbidden_paths "$scope" "$current_snapshot" - if ! cmp -s "$baseline" "$current_snapshot"; then - echo "Detected out-of-scope filesystem changes (escaped paths shown):" >&2 - diff -u "$baseline" "$current_snapshot" >&2 || true + scope_unchanged "$baseline" "$current_snapshot" || post_creation_fail "$phase exceeded its allowed file scope." - fi } requirements_signature() { diff --git a/scripts/triage-review.sh b/scripts/triage-review.sh index 8704f28..e9cdacd 100755 --- a/scripts/triage-review.sh +++ b/scripts/triage-review.sh @@ -49,7 +49,7 @@ fi review_data_require_jq if [[ "$(jq -r '.kind // "feature"' "$review_path" 2>/dev/null)" == "planning" ]]; then - fail "this is a planning review; it is not triaged. Revise the planning documents and run ./scripts/review-planning.sh again." + fail "this is a planning review; it is not triaged. Handle it with ./scripts/revise-planning.sh and review again with ./scripts/review-planning.sh." fi agent_resolve "$root" triage "$AGENT_CLI_PROVIDER" "$AGENT_CLI_MODEL" diff --git a/scripts/verify.conf b/scripts/verify.conf index 8f7a0a7..ac490f8 100644 --- a/scripts/verify.conf +++ b/scripts/verify.conf @@ -31,6 +31,7 @@ fingerprint: ./tests/fingerprint-test.sh create-feature-issue: ./tests/create-feature-issue-test.sh review-feature: ./tests/review-feature-test.sh review-planning: ./tests/review-planning-test.sh +revise-planning: ./tests/revise-planning-test.sh triage-review: ./tests/triage-review-test.sh apply-triage: ./tests/apply-triage-test.sh start-planning: ./tests/start-planning-test.sh diff --git a/tests/lib-fakes.sh b/tests/lib-fakes.sh index d68808e..ef73fa0 100644 --- a/tests/lib-fakes.sh +++ b/tests/lib-fakes.sh @@ -52,6 +52,15 @@ done if [[ -n "${MOCK_AGENT_ACTION:-}" ]]; then eval "$MOCK_AGENT_ACTION" fi +# MOCK_WRITE_ACTION and MOCK_WRITE_EXIT apply only to write-capable sessions. +if [[ "$*" == *"--sandbox workspace-write"* || "$*" == *"--permission-mode acceptEdits"* ]]; then + if [[ -n "${MOCK_WRITE_ACTION:-}" ]]; then + eval "$MOCK_WRITE_ACTION" + fi + if [[ -n "${MOCK_WRITE_EXIT:-}" ]]; then + exit "$MOCK_WRITE_EXIT" + fi +fi if [[ "$agent" == "codex" ]]; then if [[ -n "$output_file" ]]; then @@ -90,8 +99,9 @@ copy_workflow() { } # record_reviewed_tree [triage-json] -# Stores the current fingerprint of the repository as the reviewed tree of a -# fixture review, and copies it into a triage fixture, so the review is current. +# Stores the current fingerprint of what a fixture review covers (its +# reviewed_paths, or the whole repository) as its reviewed tree, and copies it +# into a triage fixture, so the review is current. record_reviewed_tree() { local repo="$1" local review="$2" @@ -102,7 +112,7 @@ record_reviewed_tree() { scratch="$(mktemp -d "${TMPDIR:-/tmp}/fingerprint.XXXXXX")" tree="$( source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)/scripts/lib/fingerprint.sh" - fingerprint_worktree "$repo" "$scratch" + fingerprint_review "$repo" "$scratch" "$review" )" rm -rf "$scratch" diff --git a/tests/revise-planning-test.sh b/tests/revise-planning-test.sh new file mode 100755 index 0000000..af8413c --- /dev/null +++ b/tests/revise-planning-test.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +tmp="$(mktemp -d "${TMPDIR:-/tmp}/revise-planning-test.XXXXXX")" + +cleanup() { + rm -rf "$tmp" +} +trap cleanup EXIT + +fail() { + echo "revise-planning test failed: $*" >&2 + exit 1 +} + +source "$source_root/tests/lib-fakes.sh" +make_fake_agents "$tmp/bin" + +review=".agents/reviews/planning-project-bootstrap-review-01.json" +revision=".agents/reviews/planning-project-bootstrap-review-01-revision" +valid_decisions='{"decisions": [ + {"finding_id": "M1", "decision": "ADOPT", "rationale": "Offline use is an MVP requirement."}, + {"finding_id": "MIN1", "decision": "REJECT", "rationale": "The name follows the requirements."}, + {"finding_id": "S1", "decision": "ESCALATE", "rationale": "Sharing scope is a product decision."} +]}' +export MOCK_OUTPUT="$valid_decisions" + +# Creates a planning repository with a current planning review of round 1. +setup_repo() { + local repo="$tmp/$1" + + mkdir -p "$repo/docs/decisions" "$repo/.agents/reviews" + copy_workflow "$repo" + printf 'project-planner: claude model-p\n' >"$repo/.agents/agents.conf" + printf '# Agents\n' >"$repo/AGENTS.md" + printf '# Project Requirements\n\nStatus: Approved\nApproved at: 2026-01-01T00:00:00Z\n\n## MVP scope\n\nOffline recipes.\n' \ + >"$repo/docs/PROJECT_REQUIREMENTS.md" + printf '# Architecture\n\nLocal storage.\n' >"$repo/docs/architecture.md" + printf '# Roadmap\n\n## F01 — Recipes\n\n- Goal: list recipes.\n' >"$repo/docs/roadmap.md" + git -C "$repo" init -q -b main + git -C "$repo" config user.name "Revise Test" + git -C "$repo" config user.email "revise-test@example.com" + git -C "$repo" add . + git -C "$repo" commit -qm "Seed" + git -C "$repo" switch -q -c planning/project-bootstrap + + jq -n '{ + schema: "review/v1", kind: "planning", issue: null, round: 1, + branch: "planning/project-bootstrap", base: "origin/main", merge_base: "aaaa", head: "bbbb", + reviewed_tree: "", reviewed_paths: ["docs/PROJECT_DESCRIPTION.md", "docs/PROJECT_REQUIREMENTS.md", "docs/architecture.md", "docs/roadmap.md", "docs/decisions"], + reviewer: {agent: "codex", model: "model-r"}, created_at: "2026-01-01T00:00:00Z", + verdict: "CHANGES_REQUIRED", limitations: "", + findings: [ + {id: "M1", severity: "major", title: "Offline use is unplanned", evidence: "docs/roadmap.md", impact: "An MVP requirement is missing.", recommendation: "Add a feature for offline use."}, + {id: "MIN1", severity: "minor", title: "Feature name is vague", evidence: "docs/roadmap.md, F01", impact: "Readability.", recommendation: "Rename F01."}, + {id: "S1", severity: "suggestion", title: "Consider sharing", evidence: "docs/roadmap.md", impact: "Households share recipes.", recommendation: "Add sharing."} + ] + }' >"$repo/$review" + record_reviewed_tree "$repo" "$repo/$review" + + printf '%s\n' "$repo" +} + +run_revise() { + local repo="$1" + local answer="$2" + shift 2 + + ( + cd "$repo" + printf '%s\n' "$answer" | + PATH="$tmp/bin:/usr/bin:/bin" MOCK_AGENT_LOG="$repo.log" ./scripts/revise-planning.sh "$@" + ) >"$repo.out" 2>&1 +} + +write_sessions() { + grep -c -- "--permission-mode acceptEdits" "$1.log" 2>/dev/null || true +} + +add_offline_feature="printf '\\n## F02 — Offline use\\n\\n- Goal: work offline.\\n' >>docs/roadmap.md" + +# The planner decides read-only; after approval the decisions are recorded and +# a write session revises exactly the adopted findings. +repo="$(setup_repo revise)" +MOCK_WRITE_ACTION="$add_offline_feature" run_revise "$repo" y --review "$review" || { + cat "$repo.out" >&2 + fail "revising the planning failed" +} +[[ -f "$repo/$revision.json" && -f "$repo/$revision.md" ]] || fail "the revision and its report were not recorded" +[[ "$(jq -c '[.schema, .review_round, (.decisions | map(.decision))]' "$repo/$revision.json")" == \ + '["revision/v1",1,["ADOPT","REJECT","ESCALATE"]]' ]] || fail "the recorded decisions are wrong" +grep -Fq -- "--tools Read,Glob,Grep" "$repo.log" || fail "the planner did not decide read-only" +grep -Fq -- "--json-schema" "$repo.log" || fail "the planner was not given the revision schema" +[[ "$(write_sessions "$repo")" -eq 1 ]] || fail "exactly one write session was expected" +grep -Fq "M1. Offline use is unplanned" "$repo.log" || fail "the adopted finding was not passed to the planner" +if grep -Fq "Feature name is vague" "$repo.log" || grep -Fq "Consider sharing" "$repo.log"; then + fail "a finding that was not adopted was passed to the write session" +fi +grep -Fq "F02 — Offline use" "$repo/docs/roadmap.md" || fail "the revision did not reach the roadmap" +grep -Fq "Escalated findings need your decision" "$repo.out" || fail "the escalated finding was not reported" +status=0 +(cd "$repo" && ./scripts/check-review.sh "$review" >/dev/null) || status=$? +[[ "$status" -eq 1 ]] || fail "the planning review is not stale after the revision" + +# A revised review cannot be revised again; the next step is a new review. +if run_revise "$repo" y --review "$review"; then + fail "an already revised review was revised again" +fi +grep -Fq "review-planning.sh" "$repo.out" || fail "the next review round was not suggested" + +# Declining records nothing and changes nothing. +repo="$(setup_repo decline)" +run_revise "$repo" n --review "$review" || fail "declining returned an error" +[[ ! -e "$repo/$revision.json" ]] || fail "declined decisions were recorded" +[[ "$(write_sessions "$repo")" -eq 0 ]] || fail "a write session started after declining" + +# Invalid decisions are retried once and then rejected before anything is recorded. +# One invalid result per rule, each derived from the valid decisions. +for invalid in \ + "not json" \ + '{"decisions": {}}' \ + "$(jq '.decisions[0].decision = "REJECT"' <<<"$valid_decisions")" \ + "$(jq '.decisions[0].decision = "DEFER"' <<<"$valid_decisions")" \ + "$(jq 'del(.decisions[2])' <<<"$valid_decisions")" \ + "$(jq '.decisions += [.decisions[1]]' <<<"$valid_decisions")" \ + "$(jq '.decisions[1].finding_id = "MIN9"' <<<"$valid_decisions")" \ + "$(jq '.decisions[1].decision = "LATER"' <<<"$valid_decisions")" \ + "$(jq '.decisions[1].rationale = ""' <<<"$valid_decisions")" \ + "$(jq '.decisions[1].severity = "minor"' <<<"$valid_decisions")"; do + repo="$(setup_repo "invalid-$(grep -c . "$tmp/invalid-count" 2>/dev/null || true)")" + echo x >>"$tmp/invalid-count" + if MOCK_OUTPUT="$invalid" run_revise "$repo" y --review "$review"; then + fail "invalid revision decisions were accepted" + fi + [[ ! -e "$repo/$revision.json" ]] || fail "invalid decisions were recorded" + [[ "$(grep -c '^AGENT=' "$repo.log")" -eq 2 ]] || fail "invalid decisions were not retried exactly once" +done + +# The write session may change only the architecture, roadmap, and ADRs. +for action in \ + "printf 'changed\\n' >>docs/PROJECT_REQUIREMENTS.md" \ + "printf 'changed\\n' >>AGENTS.md" \ + "printf 'x\\n' >.env" \ + "printf 'x\\n' >>$revision.json" \ + "printf 'x\\n' >docs/decisions/tool.sh" \ + "git add -A && git commit -qm 'Agent must not commit'"; do + repo="$(setup_repo "scope-$(grep -c . "$tmp/scope-count" 2>/dev/null || true)")" + echo x >>"$tmp/scope-count" + if MOCK_WRITE_ACTION="$add_offline_feature; $action" run_revise "$repo" y --review "$review"; then + fail "an out-of-scope change was accepted: $action" + fi + grep -Fq "preserved" "$repo.out" || fail "a scope violation did not preserve the worktree: $action" +done + +# A minor finding may be deferred. +repo="$(setup_repo defer)" +MOCK_OUTPUT="$(jq '.decisions[1].decision = "DEFER"' <<<"$valid_decisions")" \ + MOCK_WRITE_ACTION="$add_offline_feature" run_revise "$repo" y --review "$review" || { + cat "$repo.out" >&2 + fail "deferring a minor finding was rejected" +} + +# A write session that changes nothing does not count as a revision; the +# approved decisions stay for a retry. +repo="$(setup_repo no-op)" +if run_revise "$repo" y --review "$review"; then + fail "a write session without changes was reported as a revision" +fi +grep -Fq "changed no planning document" "$repo.out" || fail "an empty revision was not reported" +[[ -f "$repo/$revision.json" ]] || fail "the approved decisions were lost after an empty revision" + +# A failed write session keeps the approved decisions; while the review is +# still current, a re-run applies them without deciding again. +repo="$(setup_repo resume)" +if MOCK_WRITE_EXIT=5 run_revise "$repo" y --review "$review"; then + fail "a failed planner returned success" +fi +[[ -f "$repo/$revision.json" ]] || fail "the approved decisions were lost after a failed write session" +rm "$repo.log" +MOCK_WRITE_ACTION="$add_offline_feature" run_revise "$repo" y --review "$review" || { + cat "$repo.out" >&2 + fail "resuming the revision failed" +} +[[ "$(grep -c '^AGENT=' "$repo.log")" -eq 1 ]] || fail "resuming decided again instead of applying the recorded decisions" + +# Without adopted findings no document is changed. +repo="$(setup_repo nothing-adopted)" +MOCK_OUTPUT="$(jq '.decisions[0].decision = "ESCALATE"' <<<"$valid_decisions")" run_revise "$repo" y --review "$review" || { + cat "$repo.out" >&2 + fail "a revision without adopted findings failed" +} +[[ "$(write_sessions "$repo")" -eq 0 ]] || fail "a write session started without adopted findings" + +# A stale review is refused even when it has no findings. +repo="$(setup_repo stale-empty)" +jq '.verdict = "PASS" | .findings = []' "$repo/$review" >"$repo/$review.tmp" +mv "$repo/$review.tmp" "$repo/$review" +printf '\nChanged.\n' >>"$repo/docs/roadmap.md" +if run_revise "$repo" y --review "$review"; then + fail "a stale review without findings was accepted" +fi + +# Stale, foreign, and misplaced reviews fail before an agent starts. +repo="$(setup_repo stale)" +printf '\nChanged.\n' >>"$repo/docs/architecture.md" +if run_revise "$repo" y --review "$review"; then + fail "a stale planning review was revised" +fi +[[ ! -e "$repo.log" ]] || fail "an agent started for a stale review" + +repo="$(setup_repo feature-review)" +jq '.kind = "feature" | .issue = 3 | .reviewed_paths = null' "$repo/$review" >"$repo/$review.tmp" +mv "$repo/$review.tmp" "$repo/$review" +if run_revise "$repo" y --review "$review"; then + fail "a feature review was revised as planning" +fi + +repo="$(setup_repo not-planning)" +git -C "$repo" switch -q -c feature/1-x +if run_revise "$repo" y --review "$review"; then + fail "revision ran outside a planning worktree" +fi +[[ ! -e "$repo.log" ]] || fail "an agent started despite failed preconditions" + +echo "revise-planning tests passed" diff --git a/tests/start-planning-test.sh b/tests/start-planning-test.sh index b4cf92e..b50790e 100755 --- a/tests/start-planning-test.sh +++ b/tests/start-planning-test.sh @@ -217,7 +217,7 @@ setup_repo() { cp "$script_source" "$seed/scripts/start-planning.sh" mkdir -p "$seed/scripts/lib" - cp "$root/scripts/lib/agent.sh" "$seed/scripts/lib/agent.sh" + cp "$root"/scripts/lib/*.sh "$seed/scripts/lib/" printf 'project-grill: codex astra\nproject-planner: codex astra\n' >"$seed/.agents/agents.conf" cp "$root/.agents/prompts/project-grill.md" "$seed/.agents/prompts/project-grill.md" cp "$root/.agents/prompts/project-planner.md" "$seed/.agents/prompts/project-planner.md" From eb848dd2b0eed63787ab2a99ae693b9ff1264867 Mon Sep 17 00:00:00 2001 From: Ahmet Taspinar Date: Fri, 2 Oct 2026 23:37:03 +0200 Subject: [PATCH 2/2] Run the tests without the user's global Git configuration The review-planning test committed in a cloned repository without a Git identity, which passed locally through the global configuration and failed on CI. The clone now sets its identity, and every test suite runs with GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1, as on CI. Co-Authored-By: Claude Opus 5.5 --- tests/agent-test.sh | 4 ++++ tests/apply-triage-test.sh | 4 ++++ tests/create-feature-issue-test.sh | 4 ++++ tests/doctor-test.sh | 4 ++++ tests/fingerprint-test.sh | 4 ++++ tests/review-feature-test.sh | 4 ++++ tests/review-planning-test.sh | 6 ++++++ tests/revise-planning-test.sh | 4 ++++ tests/start-planning-test.sh | 4 ++++ tests/triage-review-test.sh | 4 ++++ tests/verify-test.sh | 4 ++++ 11 files changed, 46 insertions(+) diff --git a/tests/agent-test.sh b/tests/agent-test.sh index 3e8d1a6..766f37a 100755 --- a/tests/agent-test.sh +++ b/tests/agent-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/agent-test.XXXXXX")" diff --git a/tests/apply-triage-test.sh b/tests/apply-triage-test.sh index 41c57f1..ec62922 100755 --- a/tests/apply-triage-test.sh +++ b/tests/apply-triage-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/apply-triage-test.XXXXXX")" diff --git a/tests/create-feature-issue-test.sh b/tests/create-feature-issue-test.sh index bf417a7..abecad3 100755 --- a/tests/create-feature-issue-test.sh +++ b/tests/create-feature-issue-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/create-feature-issue-test.XXXXXX")" diff --git a/tests/doctor-test.sh b/tests/doctor-test.sh index c384661..d6c590d 100755 --- a/tests/doctor-test.sh +++ b/tests/doctor-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" script="$source_root/scripts/doctor.sh" bash_path="$(command -v bash)" diff --git a/tests/fingerprint-test.sh b/tests/fingerprint-test.sh index 85fb1b7..33badaa 100755 --- a/tests/fingerprint-test.sh +++ b/tests/fingerprint-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/fingerprint-test.XXXXXX")" diff --git a/tests/review-feature-test.sh b/tests/review-feature-test.sh index 558531b..f73e82e 100755 --- a/tests/review-feature-test.sh +++ b/tests/review-feature-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/review-feature-test.XXXXXX")" diff --git a/tests/review-planning-test.sh b/tests/review-planning-test.sh index aae65d0..78a02cc 100755 --- a/tests/review-planning-test.sh +++ b/tests/review-planning-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/review-planning-test.XXXXXX")" @@ -46,6 +50,8 @@ setup_repo() { git init -q --bare -b main "$remote" git -C "$seed" push -q "$remote" main git clone -q "$remote" "$repo" + git -C "$repo" config user.name "Planning Review Test" + git -C "$repo" config user.email "planning-review-test@example.com" git -C "$repo" switch -q -c planning/project-bootstrap printf 'A recipe organizer that works offline.\n' >"$repo/docs/PROJECT_DESCRIPTION.md" diff --git a/tests/revise-planning-test.sh b/tests/revise-planning-test.sh index af8413c..9e82407 100755 --- a/tests/revise-planning-test.sh +++ b/tests/revise-planning-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/revise-planning-test.XXXXXX")" diff --git a/tests/start-planning-test.sh b/tests/start-planning-test.sh index b50790e..6960df6 100755 --- a/tests/start-planning-test.sh +++ b/tests/start-planning-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + root="$(git rev-parse --show-toplevel)" script_source="$root/scripts/start-planning.sh" tmp="$(mktemp -d "${TMPDIR:-/tmp}/start-planning-test.XXXXXX")" diff --git a/tests/triage-review-test.sh b/tests/triage-review-test.sh index 552050c..2b27ab8 100755 --- a/tests/triage-review-test.sh +++ b/tests/triage-review-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/triage-review-test.XXXXXX")" diff --git a/tests/verify-test.sh b/tests/verify-test.sh index b7e7fea..c9a263d 100755 --- a/tests/verify-test.sh +++ b/tests/verify-test.sh @@ -2,6 +2,10 @@ set -euo pipefail +# Run as on CI: without the user's global or system Git configuration, so a +# test cannot depend on a local Git identity or setting. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 + source_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" tmp="$(mktemp -d "${TMPDIR:-/tmp}/verify-test.XXXXXX")"