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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ A lightweight, model-agnostic repository template for agentic software engineeri
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.
5. Create a GitHub Issue only for the next actionable roadmap feature. For
non-trivial work, create `.agents/plans/<issue>-<slug>.md` using
5. Create a GitHub Issue only for the next actionable roadmap feature, from its
block in `docs/roadmap.md`:

```bash
./scripts/create-feature-issue.sh F01
```

For non-trivial work, create `.agents/plans/<issue>-<slug>.md` using
`.agents/prompts/planner.md`.
6. Start the feature and implementation agent:

Expand Down
4 changes: 3 additions & 1 deletion docs/agentic-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ GitHub Issues:
#2 F02 — optional next
```

Before starting F03, review the roadmap again and only then create its Issue.
Before starting F03, review the roadmap again and only then create its Issue
with `./scripts/create-feature-issue.sh F03`, which copies the F03 block from
the roadmap into the Issue after your approval.

GitHub Issues are the actionable source of truth once created.
The roadmap remains the higher-level planning document.
Expand Down
29 changes: 29 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,35 @@ 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.

## Creating a feature Issue from the roadmap

When a roadmap feature becomes active, create its Issue from its block in
`docs/roadmap.md`:

```bash
./scripts/create-feature-issue.sh F03
```

The script finds the heading that starts with the feature ID, such as
`### F03 — Sharing`, and uses everything up to the next heading of the same or
a higher level as the Issue body, unchanged. The heading becomes the title, so
the feature ID stays visible and review triage can use it for follow-up
provenance. The script shows the proposed Issue and creates it only after your
approval.

It refuses a feature ID that is missing, used for more than one heading, or
has an empty block, and it does not create a second Issue for a feature that
already has one, open or closed. When the block refers to another feature
whose Issue is still open, it warns before asking.

Create Issues one feature at a time, only for work that is about to start. An
Issue that is not a roadmap feature can still be created from a title and a
body file:

```bash
./scripts/create-feature-issue.sh "Issue title" path/to/body.md [label]
```

## Canonical feature workflow

Start with an actionable GitHub Issue. Create a detailed implementation plan
Expand Down
234 changes: 195 additions & 39 deletions scripts/create-feature-issue.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,213 @@

set -euo pipefail

TITLE="${1:-}"
BODY_FILE="${2:-}"
LABEL="${3:-}"
# 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.

usage() {
echo "Usage:"
echo " $0 <feature-id>"
echo " $0 \"Issue title\" path/to/body.md [label]"
echo
echo "With a feature ID such as F03, the Issue is created from the block under the"
echo "roadmap heading that starts with that ID, after your approval."
exit 1
}

if ! command -v gh >/dev/null 2>&1; then
echo "Error: GitHub CLI 'gh' is not installed."
fail() {
echo "Error: $*" >&2
exit 1
fi
}

if ! gh auth status >/dev/null 2>&1; then
echo "Error: GitHub CLI is not authenticated."
echo "Run: gh auth login"
exit 1
fi
require_gh() {
command -v gh >/dev/null 2>&1 || fail "GitHub CLI 'gh' is not installed."
gh auth status >/dev/null 2>&1 || fail "GitHub CLI is not authenticated. Run: gh auth login"
}

if [[ -z "$TITLE" || -z "$BODY_FILE" ]]; then
usage
fi
# Explicit title and body file.
create_from_file() {
local title="$1"
local body_file="$2"
local label="${3:-}"
local args=(issue create --title "$title" --body-file "$body_file")
local issue_url

if [[ ! -f "$BODY_FILE" ]]; then
echo "Error: body file not found: $BODY_FILE"
exit 1
fi

ARGS=(
issue create
--title "$TITLE"
--body-file "$BODY_FILE"
)

if [[ -n "$LABEL" ]]; then
if gh label list --limit 100 --json name --jq '.[].name' | grep -Fxq "$LABEL"; then
ARGS+=(--label "$LABEL")
else
echo "Warning: label '$LABEL' does not exist."
echo "Creating issue without label."
[[ -f "$body_file" ]] || fail "body file not found: $body_file"
require_gh

if [[ -n "$label" ]]; then
if gh label list --limit 100 --json name --jq '.[].name' | grep -Fxq "$label"; then
args+=(--label "$label")
else
echo "Warning: label '$label' does not exist."
echo "Creating issue without label."
fi
fi
fi

echo "Creating GitHub issue:"
echo " Title: $TITLE"
echo " Body: $BODY_FILE"
echo "Creating GitHub issue:"
echo " Title: $title"
echo " Body: $body_file"

issue_url="$(gh "${args[@]}")"

echo
echo "Issue created:"
echo "$issue_url"
}

# Prints the Issues whose title names the feature ID as "<number>\t<state>\t<title>".
issues_for_feature() {
local feature="$1"

gh issue list --state all --limit 200 --search "$feature in:title" --json number,state,title </dev/null |
jq -r --arg feature "$feature" '
.[] | select(.title | test("(^|[^A-Za-z0-9])" + $feature + "([^A-Za-z0-9]|$)"))
| "\(.number)\t\(.state)\t\(.title)"'
}

# roadmap_section <roadmap> <feature-id> <count|title|block>
# Reads Markdown headings, ignoring lines inside fenced code blocks. Prints the
# number of headings that start with the feature ID, the first such heading
# without its markers, or the lines below it up to the next heading of the
# same or a higher level.
roadmap_section() {
awk -v id="$2" -v mode="$3" '
function starts_with_id(text) {
return index(text, id) == 1 && substr(text, length(id) + 1, 1) !~ /[A-Za-z0-9]/
}
/^[[:space:]]*(```|~~~)/ {
fenced = !fenced
if (inside) { print }
next
}
!fenced && /^#+[[:space:]]/ {
level = match($0, /[^#]/) - 1
text = $0
sub(/^#+[[:space:]]+/, "", text)
sub(/[[:space:]]+$/, "", text)
if (inside && level <= block_level) {
exit
}
if (starts_with_id(text)) {
count++
if (mode == "title" && count == 1) {
print text
exit
}
if (mode == "block" && !inside) {
inside = 1
block_level = level
next
}
}
}
inside { print }
END {
if (mode == "count") {
print count + 0
}
}
' "$1"
}

ISSUE_URL="$(gh "${ARGS[@]}")"
create_from_roadmap() {
local feature="$1"
local root
local roadmap
local heading_count
local title
local block
local existing
local reference
local open_issue
local approval=""
local issue_url

root="$(git rev-parse --show-toplevel)"
roadmap="$root/docs/roadmap.md"
[[ -f "$roadmap" ]] || fail "roadmap not found: docs/roadmap.md"
command -v jq >/dev/null 2>&1 || fail "jq is required."

# The heading ID is the only machine-readable part of the roadmap.
heading_count="$(roadmap_section "$roadmap" "$feature" count)"
[[ "$heading_count" -ne 0 ]] || fail "no roadmap heading starts with $feature in docs/roadmap.md."
[[ "$heading_count" -eq 1 ]] || fail "$heading_count roadmap headings start with $feature; feature IDs must be unique."

title="$(roadmap_section "$roadmap" "$feature" title)"
block="$(roadmap_section "$roadmap" "$feature" block)"
[[ "$block" =~ [^[:space:]] ]] || fail "the roadmap block of $feature is empty."

require_gh

existing="$(issues_for_feature "$feature")" ||
fail "could not list the existing Issues for $feature; no Issue was created."
if [[ -n "$existing" ]]; then
echo "Error: $feature already has an Issue; no new Issue was created:" >&2
printf '%s\n' "$existing" | awk -F '\t' '{ printf " #%s (%s) %s\n", $1, tolower($2), $3 }' >&2
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
echo "---"
echo
echo "Source: \`docs/roadmap.md\`, $feature."
} >"$tmp_work/body.md"

echo "Proposed Issue for roadmap feature $feature:"
echo
echo "Title: $title"
echo
cat "$tmp_work/body.md"
echo

# Features this block refers to that are not done yet.
while IFS= read -r reference; do
[[ "$reference" != "$feature" ]] || continue
if ! reference_issues="$(issues_for_feature "$reference")"; then
echo "Warning: could not check whether $reference, which $feature refers to, is done."
open_issue=1
continue
fi
while IFS=$'\t' read -r number state issue_title; do
[[ -n "$number" ]] || continue
if [[ "$state" == "OPEN" ]]; then
echo "Warning: $feature refers to $reference, whose Issue #$number is still open: $issue_title"
open_issue=1
fi
done <<<"$reference_issues"
done < <(printf '%s\n' "$block" | grep -oE '(^|[^A-Za-z0-9])F[0-9]+' | grep -oE 'F[0-9]+' | sort -u)
[[ -z "${open_issue:-}" ]] || echo

printf "Create this Issue? [y/N] "
read -r approval || true
case "$approval" in
y | Y | yes | YES) ;;
*)
echo "Declined; no Issue was created."
exit 0
;;
esac

issue_url="$(gh issue create --title "$title" --body-file "$tmp_work/body.md" </dev/null | tail -n 1)"
echo
echo "Issue created for $feature:"
echo "$issue_url"
}

echo
echo "Issue created:"
echo "$ISSUE_URL"
case "$#" in
1)
[[ "$1" =~ ^F[0-9]+$ ]] || fail "feature ID must look like F03: $1"
create_from_roadmap "$1"
;;
2 | 3)
create_from_file "$@"
;;
*)
usage
;;
esac
1 change: 1 addition & 0 deletions scripts/verify.conf
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ verify: ./tests/verify-test.sh
doctor: ./tests/doctor-test.sh
agent: ./tests/agent-test.sh
fingerprint: ./tests/fingerprint-test.sh
create-feature-issue: ./tests/create-feature-issue-test.sh
review-feature: ./tests/review-feature-test.sh
triage-review: ./tests/triage-review-test.sh
apply-triage: ./tests/apply-triage-test.sh
Expand Down
Loading
Loading