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
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,18 @@ A lightweight, model-agnostic repository template for agentic software engineeri
`docs/PROJECT_REQUIREMENTS.md`. After you explicitly approve those
requirements, a separate project-planning session creates the architecture,
necessary ADRs, and roadmap. The script does not commit or push.
4. Review the planning worktree, run `./scripts/verify.sh`, commit and push the
planning branch, then merge it through a PR before feature development.
4. In the planning worktree, review and revise the planning until the review
passes, then approve it:

```bash
./scripts/review-planning.sh
./scripts/revise-planning.sh --review .agents/reviews/planning-project-bootstrap-review-01.json
./scripts/review-planning.sh
./scripts/finish-planning.sh
```

Commit and push the planning branch as `finish-planning.sh` shows, then merge
it through a PR before feature development.
5. Create a GitHub Issue only for the next actionable roadmap feature, from its
block in `docs/roadmap.md`:

Expand Down
17 changes: 11 additions & 6 deletions docs/agentic-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,17 @@ Recommended sequence:
- `docs/roadmap.md`
- required ADRs under `docs/decisions/`

8. Review and verify the bootstrap artifacts.
9. Commit and push the planning branch.
10. Open a Pull Request.
11. Merge the approved bootstrap into `main`.
12. Convert only ready roadmap items into GitHub Issues.
13. Start feature clarification and development.
8. An independent agent reviews the planning (`review-planning.sh`). The
planner decides per finding and revises the adopted ones
(`revise-planning.sh`). Repeat until the review passes.
9. Approve the planning with `finish-planning.sh`, which records
`docs/PLANNING_APPROVAL.md`.
10. Verify, commit, and push the planning branch.
11. Open a Pull Request.
12. Merge the approved bootstrap into `main`.
13. Convert only ready roadmap items into GitHub Issues with
`create-feature-issue.sh`, which requires a current planning approval.
14. Start feature clarification and development.

Declining requirements approval or an agent failure preserves the worktree and
stops later phases. The script does not fall back to another model, implement
Expand Down
50 changes: 39 additions & 11 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,9 @@ Declining requirements approval or an agent failure stops the workflow and
leaves the planning worktree intact. Inspect or revise it there; the script
never substitutes another model or removes user work automatically.

After successful planning, review the artifacts and complete the process
manually:

```bash
cd ../<repository>-planning-project-bootstrap
./scripts/verify.sh
git add docs/PROJECT_DESCRIPTION.md # only when --description was used
git add docs/PROJECT_REQUIREMENTS.md docs/architecture.md docs/roadmap.md docs/decisions
git commit -m "Plan project bootstrap"
git push -u origin planning/project-bootstrap
```
After successful planning, continue in the planning worktree with the planning
review, revision, and approval described below. `finish-planning.sh` prints
the exact commit and push commands.

### Planning review

Expand Down Expand Up @@ -218,6 +210,42 @@ 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.

### Planning approval

When the latest review round is resolved, approve the planning:

```bash
./scripts/finish-planning.sh
```

The script refuses when a required document is missing, the requirements are
not approved, there is no planning review, the latest review is stale, the
latest round has a critical or major finding, a finding of the latest round
has no revision decision, an adopted finding is not applied yet, or a finding
of the latest round is escalated. It shows every rejected, deferred, and
escalated finding of all rounds, also before a refusal. A finding escalated in
an earlier round is not resolved by a newer review alone: you confirm that it
was resolved, and the approval records that. The approval covers exactly the
reviewed planning; a document that changes while the script waits for your
answer is refused.

The approval is recorded in `docs/PLANNING_APPROVAL.md`, which is committed with
the planning: the time, the planning branch, the final review round, the
review rounds, the findings that were not adopted, and the fingerprint of the
planning documents. Use it for the planning PR description, because review
and revision files are not committed.

Any later change to a planning document invalidates the approval:

```bash
./scripts/finish-planning.sh --check
```

exits 0 when the approval still matches the documents and 1 when it is missing
or stale. `create-feature-issue.sh <feature-id>` refuses to create a feature
Issue from a roadmap without a current approval, so a roadmap change after the
planning PR needs a new review round and approval.

Open and merge a planning PR before creating Issues for actionable roadmap
features. The script never implements features, creates Issues, commits,
pushes, opens or merges a PR, or deploys.
Expand Down
16 changes: 13 additions & 3 deletions scripts/create-feature-issue.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ set -euo pipefail
# Creates the GitHub Issue for one roadmap feature from its block in
# docs/roadmap.md, or an Issue from an explicit title and body file.

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
source "$script_dir/lib/fingerprint.sh"
source "$script_dir/lib/planning.sh"

usage() {
echo "Usage:"
echo " $0 <feature-id>"
Expand Down Expand Up @@ -138,6 +142,15 @@ create_from_roadmap() {
block="$(roadmap_section "$roadmap" "$feature" block)"
[[ "$block" =~ [^[:space:]] ]] || fail "the roadmap block of $feature is empty."

# Features start only from a roadmap whose planning is approved as it is now.
tmp_work="$(mktemp -d "${TMPDIR:-/tmp}/create-feature-issue.XXXXXX")"
trap 'rm -rf "$tmp_work"' EXIT
local approval_status=0
local approval_reason
approval_reason="$(planning_approval_status "$root" "$tmp_work")" || approval_status=$?
[[ "$approval_status" -eq 0 ]] ||
fail "$approval_reason Approve the planning with ./scripts/finish-planning.sh before creating feature Issues."

require_gh

existing="$(issues_for_feature "$feature")" ||
Expand All @@ -148,9 +161,6 @@ create_from_roadmap() {
exit 1
fi

# Global, so the exit trap can still remove it after this function returns.
tmp_work="$(mktemp -d "${TMPDIR:-/tmp}/create-feature-issue.XXXXXX")"
trap 'rm -rf "$tmp_work"' EXIT
{
printf '%s\n' "$block" | awk 'NF { found = 1 } found' | awk '{ lines[NR] = $0 } END { last = NR; while (last > 0 && lines[last] !~ /[^[:space:]]/) last--; for (i = 1; i <= last; i++) print lines[i] }'
echo
Expand Down
215 changes: 215 additions & 0 deletions scripts/finish-planning.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#!/usr/bin/env bash

set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
source "$script_dir/lib/review-data.sh"
source "$script_dir/lib/fingerprint.sh"
source "$script_dir/lib/planning.sh"

usage() {
echo "Usage:"
echo " $0 Check the planning and record your approval (planning worktree)."
echo " $0 --check Report whether the recorded approval still matches the documents."
echo
echo "--check exits 0 when the approval is current, 1 when it is missing or stale,"
echo "and 2 on an error."
exit 2
}

fail() {
echo "Error: $*" >&2
exit 1
}

root="$(git rev-parse --show-toplevel)"
review_data_require_jq
tmp_work="$(mktemp -d "${TMPDIR:-/tmp}/finish-planning.XXXXXX")"
trap 'rm -rf "$tmp_work"' EXIT

case "$#:${1:-}" in
0:) ;;
1:--check)
status=0
reason="$(planning_approval_status "$root" "$tmp_work")" || status=$?
case "$status" in
0) echo "Current: $PLANNING_APPROVAL_FILE matches the planning documents." ;;
1) echo "Not approved: $reason"; echo "Finish the planning again with ./scripts/finish-planning.sh in a planning worktree." ;;
*) echo "Error: $reason" >&2 ;;
esac
exit "$status"
;;
*) usage ;;
esac

branch="$(git branch --show-current)"
[[ "$branch" == planning/* ]] ||
fail "finish-planning.sh must run in a planning worktree (branch planning/<name>). Current branch: $branch"
name="${branch#planning/}"

# 1. The planning documents exist and the requirements are approved.
for required in docs/PROJECT_REQUIREMENTS.md docs/architecture.md docs/roadmap.md; do
[[ -f "$root/$required" && ! -L "$root/$required" && -s "$root/$required" ]] ||
fail "required planning document is missing or empty: $required"
done
grep -Fqx "Status: Approved" "$root/docs/PROJECT_REQUIREMENTS.md" ||
fail "the project requirements are not approved."

# 2. The latest planning review exists, is valid, and is current.
shopt -s nullglob
reviews=("$root/.agents/reviews/planning-${name}-review-"[0-9][0-9].json)
shopt -u nullglob
[[ "${#reviews[@]}" -gt 0 ]] || fail "no planning review exists. Run ./scripts/review-planning.sh first."
latest="${reviews[${#reviews[@]}-1]}"
latest_relative="${latest#"$root"/}"
review_errors="$(review_artifact_errors "$latest")"
[[ -z "$review_errors" ]] || fail "the latest planning review is invalid: ${review_errors//$'\n'/; }"
[[ "$(jq -r '.kind' "$latest")" == "planning" && "$(jq -r '.branch' "$latest")" == "$branch" ]] ||
fail "$latest_relative is not a planning review of $branch."

current=0
review_is_current "$root" "$tmp_work" "$latest" || current=$?
case "$current" in
0) ;;
1) fail "the latest planning review ($latest_relative) is stale: a planning document changed after it. Run ./scripts/review-planning.sh again." ;;
*) fail "could not compute the fingerprint of the planning documents." ;;
esac

# Every rejected, deferred, or escalated decision of every round, oldest first.
shopt -s nullglob
revisions=("$root/.agents/reviews/planning-${name}-review-"[0-9][0-9]-revision.json)
shopt -u nullglob
decisions_file="$tmp_work/decisions.json"
if [[ "${#revisions[@]}" -gt 0 ]]; then
jq -s '[.[] | .review_round as $round | .decisions[]
| select(.decision != "ADOPT") | . + {round: $round}]' "${revisions[@]}" >"$decisions_file"
else
echo '[]' >"$decisions_file"
fi

round="$(jq -r '.round' "$latest")"
verdict="$(jq -r '.verdict' "$latest")"

# Show every finding that was not adopted before any refusal, so the reasons
# are visible.
if [[ "$(jq 'length' "$decisions_file")" -gt 0 ]]; then
echo "Findings that were not adopted:"
jq -r '.[] | "- Round \(.round), \(.decision): \(.finding_id) [\(.severity)] \(.title)\n Rationale: \(.rationale)"' "$decisions_file"
echo
fi

# 3. The latest round is resolved: no critical or major finding, every finding
# decided, nothing adopted but not yet applied, and nothing escalated.
blocking="$(jq '[.findings[] | select(.severity == "critical" or .severity == "major")] | length' "$latest")"
[[ "$blocking" -eq 0 ]] ||
fail "the latest planning review (round $round) has $blocking critical or major finding(s). Revise with ./scripts/revise-planning.sh and review again."

revision="${latest%.json}-revision.json"
if [[ "$(jq '.findings | length' "$latest")" -gt 0 ]]; then
[[ -f "$revision" ]] ||
fail "the findings of round $round have no revision decisions. Run ./scripts/revise-planning.sh --review $latest_relative."
revision_errors="$(revision_artifact_errors "$revision" "$latest")"
[[ -z "$revision_errors" ]] || fail "the revision of round $round is invalid: ${revision_errors//$'\n'/; }"
[[ "$(jq '[.decisions[] | select(.decision == "ADOPT")] | length' "$revision")" -eq 0 ]] ||
fail "round $round has adopted findings that are not applied yet. Run ./scripts/revise-planning.sh --review $latest_relative and review again."
fi

escalated_now="$(jq --argjson round "$round" '[.[] | select(.round == $round and .decision == "ESCALATE")] | length' "$decisions_file")"
[[ "$escalated_now" -eq 0 ]] ||
fail "round $round has $escalated_now escalated finding(s). Resolve them through Project Grill and a new review round, or decide that they do not apply."

# 4. Escalations of earlier rounds need an explicit resolution by the human;
# a newer review alone does not resolve them.
earlier_escalations="$(jq -r --argjson round "$round" '
.[] | select(.round < $round and .decision == "ESCALATE") | "- Round \(.round): \(.finding_id) \(.title)"
' "$decisions_file")"
if [[ -n "$earlier_escalations" ]]; then
echo "Escalated in earlier rounds:"
printf '%s\n' "$earlier_escalations"
printf "Was each of these resolved, by changing the requirements through Project Grill or by deciding that it does not apply? [y/N] "
resolved=""
read -r resolved || true
case "$resolved" in
y | Y | yes | YES) ;;
*) fail "the escalated findings of earlier rounds are not resolved." ;;
esac
echo
fi

# 5. Ask for approval of exactly the reviewed planning.
reviewed_fingerprint="$(jq -r '.reviewed_tree' "$latest")"
echo "Planning ready for approval:"
echo " Branch: $branch"
echo " Final review: $latest_relative (round $round, ${verdict//_/ })"
echo

printf "Approve this planning? [y/N] "
approval=""
read -r approval || true
case "$approval" in
y | Y | yes | YES) ;;
*)
echo "Declined; the planning approval was not recorded."
exit 0
;;
esac

# 6. Record the approval for the reviewed planning, and only if it did not
# change while you were deciding.
fingerprint="$(fingerprint_files "$root" "$tmp_work" "${PLANNING_SCOPE[@]}")" ||
fail "could not compute the fingerprint of the planning documents."
[[ "$fingerprint" == "$reviewed_fingerprint" ]] ||
fail "a planning document changed after the final review; the approval was not recorded. Run ./scripts/review-planning.sh again."
{
echo "# Planning Approval"
echo
echo "Status: Approved"
echo "Approved at: $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "Planning branch: $branch"
echo "Final review: round $round, ${verdict//_/ }, by $(jq -r '"\(.reviewer.agent) (\(.reviewer.model))"' "$latest")"
echo "Planning fingerprint: $fingerprint"
echo
echo "Recorded by \`scripts/finish-planning.sh\`. The approval covers these planning"
echo "documents; any change to them invalidates it (\`./scripts/finish-planning.sh --check\`):"
echo
for path in "${PLANNING_SCOPE[@]}"; do
echo "- \`$path\`"
done
echo
echo "## Review rounds"
echo
for review in "${reviews[@]}"; do
jq -r '"- Round \(.round): \(.verdict | gsub("_"; " ")), \(.findings | length) finding(s), by \(.reviewer.agent) (\(.reviewer.model))"' "$review"
done
echo
if [[ -n "$earlier_escalations" ]]; then
echo "## Escalations confirmed as resolved"
echo
printf '%s\n' "$earlier_escalations"
echo
fi
echo "## Findings that were not adopted"
echo
if [[ "$(jq 'length' "$decisions_file")" -eq 0 ]]; then
echo "None."
else
jq -r '.[] | "- Round \(.round), \(.decision): \(.finding_id) [\(.severity)] \(.title). \(.rationale)"' "$decisions_file"
fi
} >"$root/$PLANNING_APPROVAL_FILE"

echo
echo "Recorded the approval in $PLANNING_APPROVAL_FILE."
echo
echo "Next steps:"
echo " ./scripts/verify.sh"
to_add=()
for path in "${PLANNING_SCOPE[@]}"; do
[[ ! -e "$root/$path" ]] || to_add+=("$path")
done
echo " git add ${to_add[*]} $PLANNING_APPROVAL_FILE"
echo " git commit -m \"Plan project bootstrap\""
echo " git push -u origin \"$branch\""
echo
echo "Open the planning PR; $PLANNING_APPROVAL_FILE summarizes the review rounds and the"
echo "findings that were not adopted for its description. After the merge, create"
echo "feature Issues with ./scripts/create-feature-issue.sh <feature-id>."
Loading
Loading