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
4 changes: 3 additions & 1 deletion .ai-context/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ the canonical current command list.
- `basectl gh <area> <command>` - manage GitHub issues, PRs, branches, repo
hygiene, and Project metadata using Base conventions.
- `basectl gh issue create` defaults to category `enhancement` when
`--category` is omitted and prints that default in command output.
`--category` is omitted and prints that default in command output. Pass
`--size <T|S|M|L>` when the issue scope is clear; otherwise Project
metadata defaults to `Size=S`.
- `basectl gh pr create` auto-injects `Fixes #<issue>` from Base branch
names; pass `--no-fixes` to suppress that body injection.
- `basectl gh project doctor --project <title>` - inspect Project metadata
Expand Down
4 changes: 4 additions & 0 deletions .ai-context/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ allows it. When an issue is tracked in the Base Roadmap Project, move its
status through `In Progress`, `In Review`, and `Done` as the work advances.
Base roadmap Project metadata uses five fields: `Status`, `Priority`, `Area`,
`Size`, and `Initiative`.
Use the smallest accurate `Size` when creating issues: `T` for tiny obvious
work, `S` for normal small work or unknown scope, `M` for interacting changes,
and `L` only for work that should probably be split. The default remains `S`
when automation cannot infer scope.

Base-managed repositories should carry `.github/workflows/project-intake.yml`
as the fallback for issues created outside `basectl gh issue create`.
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ them.
before implementation starts, move it to `In Review` when the PR opens, and
verify `Done` after merge/closure. If Project V2 access or item state
prevents an update, mention that in the work summary.
- When creating issues, choose Project `Size` from actual scope: `T` for tiny
obvious work, `S` for normal small work or unknown scope, `M` for interacting
changes, and `L` only for work that should probably be split.
- Prefer `basectl gh` for supported issue, branch, PR, check, and cleanup
operations.
- Fall back to the GitHub connector, raw `gh`, or `git` when `basectl gh` does
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,8 +550,9 @@ Base-managed default branch protection ruleset, configures a repo-named GitHub
Project copied from `base-project-template`, and creates the standard GitHub
labels documented in [Repository Baseline](docs/repo-baseline.md).
When `.github/base-project.yml` exists, `repo configure` also adds missing
repo-specific `Area` and `Initiative` Project options from that file and applies
its `issue_defaults` to Project issue items that are missing those values.
shared Project field options, adds repo-specific `Area` and `Initiative`
Project options from that file, and applies its `issue_defaults` to Project
issue items that are missing those values.
`repo init` also seeds `.github/workflows/project-intake.yml`, a visible
fallback for issues created outside `basectl gh issue create`. `repo configure`
creates the workflow when it is missing from older Base-managed repositories.
Expand Down
43 changes: 36 additions & 7 deletions cli/bash/commands/basectl/subcommands/gh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ base_gh_usage() {
cat <<'EOF'
Usage:
basectl gh issue list [gh options...]
basectl gh issue create [--category <bug|enhancement|documentation|ci|security>] --title <title> [--body <body>] [--repo <owner/name>] [project options...]
basectl gh issue create [--category <bug|enhancement|documentation|ci|security>] --title <title> [--body <body>] [--repo <owner/name>] [--size <T|S|M|L>] [project options...]
basectl gh issue start <number> [--category <bug|enhancement|documentation|ci|security>] [--title <title>]
basectl gh pr create [--no-fixes] [gh options...]
basectl gh pr status [gh options...]
Expand All @@ -34,6 +34,7 @@ Issue create project options:
--category <category> Issue label category. Defaults to enhancement.
--project <title> Project to update. Defaults to the repository name.
--project-owner <login> Project owner. Defaults to the repository owner.
--size <T|S|M|L> Project Size value. Defaults to .github/base-project.yml or S.
--no-project Skip Project metadata updates.

Issue categories:
Expand All @@ -55,7 +56,7 @@ base_gh_issue_usage() {
cat <<'EOF'
Usage:
basectl gh issue list [gh options...]
basectl gh issue create [--category <bug|enhancement|documentation|ci|security>] --title <title> [--body <body>] [--repo <owner/name>] [project options...]
basectl gh issue create [--category <bug|enhancement|documentation|ci|security>] --title <title> [--body <body>] [--repo <owner/name>] [--size <T|S|M|L>] [project options...]
basectl gh issue start <number> [--category <bug|enhancement|documentation|ci|security>] [--title <title>]

Purpose:
Expand All @@ -69,6 +70,7 @@ Issue create project options:
--category <category> Issue label category. Defaults to enhancement.
--project <title> Project to update. Defaults to the repository name.
--project-owner <login> Project owner. Defaults to the repository owner.
--size <T|S|M|L> Project Size value. Defaults to .github/base-project.yml or S.
--no-project Skip Project metadata updates.

Default category: enhancement.
Expand Down Expand Up @@ -347,6 +349,18 @@ base_gh_project_issue_set_fields() {
"$wrapper" --project base base_github_projects project issue set-fields "$@"
}

base_gh_validate_project_size() {
local size="$1"

case "$size" in
T|S|M|L)
return 0
;;
esac
base_gh_error "Invalid size '$size'. Expected one of: T, S, M, L."
return 1
}

base_gh_current_issue_from_branch() {
local branch

Expand Down Expand Up @@ -393,6 +407,7 @@ base_gh_issue_create() {
local issue_number=""
local issue_output=""
local project_owner=""
local project_size=""
local project_title=""
local title=""

Expand Down Expand Up @@ -422,6 +437,10 @@ base_gh_issue_create() {
project_owner="${2:-}"
shift
;;
--size)
project_size="${2:-}"
shift
;;
--no-project)
configure_project=0
;;
Expand All @@ -446,6 +465,9 @@ base_gh_issue_create() {
printf 'Using default --category: enhancement\n'
fi
base_gh_validate_category "$category" || return 1
if [[ -n "$project_size" ]]; then
base_gh_validate_project_size "$project_size" || return 1
fi

[[ -n "$github_repo" ]] || github_repo="$(base_gh_infer_github_repo || true)"
if [[ -n "$body" ]]; then
Expand All @@ -472,19 +494,26 @@ base_gh_issue_create() {
[[ -n "$project_owner" ]] || project_owner="$(base_gh_project_owner_from_repo "$github_repo")"
config_path="$(base_gh_project_config_path || true)"
if [[ -n "$config_path" ]]; then
base_gh_project_issue_set_fields "$issue_number" \
--project "$project_title" \
--owner "$project_owner" \
--repo "$github_repo" \
local field_args=(
"$issue_number"
--project "$project_title"
--owner "$project_owner"
--repo "$github_repo"
--config "$config_path"
)
if [[ -n "$project_size" ]]; then
field_args+=(--size "$project_size")
fi
base_gh_project_issue_set_fields "${field_args[@]}"
else
[[ -n "$project_size" ]] || project_size="S"
base_gh_project_issue_set_fields "$issue_number" \
--project "$project_title" \
--owner "$project_owner" \
--repo "$github_repo" \
--status Backlog \
--priority P2 \
--size S
--size "$project_size"
fi
fi
}
Expand Down
1 change: 1 addition & 0 deletions cli/bash/commands/basectl/tests/completions.bats
Original file line number Diff line number Diff line change
Expand Up @@ -266,5 +266,6 @@ EOF
[[ "$output" == *"--category"* ]]
[[ "$output" == *"--title"* ]]
[[ "$output" == *"--body"* ]]
[[ "$output" == *"--size"* ]]
[[ "$output" != *"--type"* ]]
}
55 changes: 55 additions & 0 deletions cli/bash/commands/basectl/tests/gh.bats
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ load ./basectl_helpers.bash
[[ "$output" == *"basectl gh issue create"* ]]
[[ "$output" == *"basectl gh issue start <number>"* ]]
[[ "$output" == *"Issue create project options:"* ]]
[[ "$output" == *"--size <T|S|M|L>"* ]]
[[ "$output" == *"Default category: enhancement."* ]]
[[ "$output" == *"Categories: bug, enhancement, documentation, ci, security."* ]]
[[ "$output" != *"basectl gh pr create"* ]]
Expand Down Expand Up @@ -234,6 +235,60 @@ EOF
[ "$(cat "$TEST_STATE_DIR/wrapper-args")" = "--project base base_github_projects project issue set-fields 51 --project bankbuddy --owner codeforester --repo codeforester/bankbuddy --config $repo_root/.github/base-project.yml" ]
}

@test "basectl gh issue create accepts explicit project size override" {
local repo
local repo_root

repo="$TEST_TMPDIR/bankbuddy"
init_git_repo "$repo"
repo_root="$(cd "$repo" && pwd -P)"
git -C "$repo" remote add origin git@github.com:codeforester/bankbuddy.git
mkdir -p "$repo/.github"
cat > "$repo/.github/base-project.yml" <<'EOF'
project:
issue_defaults:
status: Backlog
priority: P2
size: S
EOF
cat > "$TEST_MOCKBIN/gh" <<'EOF'
#!/usr/bin/env bash
if [[ "$*" == "auth status -h github.com" ]]; then
exit 0
fi
printf '%s\n' "$*" > "${BASE_GH_TEST_STATE_DIR:?}/gh-args"
if [[ "$1" == "issue" && "$2" == "create" ]]; then
printf 'https://github.com/codeforester/bankbuddy/issues/52\n'
fi
EOF
chmod +x "$TEST_MOCKBIN/gh"
cat > "$TEST_MOCKBIN/project-wrapper" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" > "${BASE_GH_TEST_STATE_DIR:?}/wrapper-args"
printf 'project metadata updated\n'
EOF
chmod +x "$TEST_MOCKBIN/project-wrapper"

run env \
HOME="$TEST_HOME" \
BASE_HOME="$BASE_REPO_ROOT" \
BASE_GH_TEST_STATE_DIR="$TEST_STATE_DIR" \
BASE_GH_PROJECT_WRAPPER="$TEST_MOCKBIN/project-wrapper" \
PATH="$TEST_MOCKBIN:$PATH" \
bash -c '
cd "$1"
source "$BASE_HOME/base_init.sh"
source "$BASE_HOME/cli/bash/commands/basectl/subcommands/gh.sh"
base_gh_subcommand_main issue create --category enhancement --title "Fix typo" --size T
' bash "$repo"

[ "$status" -eq 0 ]
[[ "$output" == *"https://github.com/codeforester/bankbuddy/issues/52"* ]]
[[ "$output" == *"project metadata updated"* ]]
[ "$(cat "$TEST_STATE_DIR/gh-args")" = "issue create --title Fix typo --label enhancement --assignee codeforester --repo codeforester/bankbuddy" ]
[ "$(cat "$TEST_STATE_DIR/wrapper-args")" = "--project base base_github_projects project issue set-fields 52 --project bankbuddy --owner codeforester --repo codeforester/bankbuddy --config $repo_root/.github/base-project.yml --size T" ]
}

@test "basectl gh issue create help does not require authentication" {
cat > "$TEST_MOCKBIN/gh" <<'EOF'
#!/usr/bin/env bash
Expand Down
9 changes: 2 additions & 7 deletions cli/python/base_github_projects/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .project_model import SelectFieldSpec, SelectOption
from .project_config import ProjectConfig, ProjectConfigError
from .project_config import read_project_config as _read_project_config
from .project_errors import missing_issue_field_option_message


class ProjectUsageError(RuntimeError):
Expand Down Expand Up @@ -389,13 +390,7 @@ def resolve_issue_field_updates(
raise ProjectUsageError(f"{field_name} field was not found in Project '{project_title}'.")
option = find_option(field, option_name)
if option is None or option.option_id is None:
if field_name == "Initiative":
raise ProjectUsageError(
f"Initiative option '{option_name}' was not found in Project '{project_title}'. "
f'Run `basectl gh project configure --project "{project_title}" '
f'--initiative-option "{option_name}"` first.'
)
raise ProjectUsageError(f"{field_name} option '{option_name}' was not found in Project '{project_title}'.")
raise ProjectUsageError(missing_issue_field_option_message(field_name, option_name, project_title))
updates.append(FieldUpdate(field.field_id, option.option_id, field_name, option_name))
return tuple(updates)

Expand Down
16 changes: 16 additions & 0 deletions cli/python/base_github_projects/project_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from __future__ import annotations


def missing_issue_field_option_message(field_name: str, option_name: str, project_title: str) -> str:
if field_name == "Initiative":
return (
f"Initiative option '{option_name}' was not found in Project '{project_title}'. "
f'Run `basectl gh project configure --project "{project_title}" '
f'--initiative-option "{option_name}"` first.'
)
if field_name == "Size":
return (
f"Size option '{option_name}' was not found in Project '{project_title}'. "
f'Run `basectl gh project configure --project "{project_title}"` first.'
)
return f"{field_name} option '{option_name}' was not found in Project '{project_title}'."
1 change: 1 addition & 0 deletions cli/python/base_github_projects/project_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class ProjectArguments:
SelectFieldSpec(
"Size",
(
SelectOption("T", "BLUE", "Tiny, obvious change with no cross-module behavior."),
SelectOption("S", "GREEN", "Small, focused change."),
SelectOption("M", "YELLOW", "Medium change with multiple files or interactions."),
SelectOption("L", "ORANGE", "Large change that should be split if possible."),
Expand Down
58 changes: 58 additions & 0 deletions cli/python/base_github_projects/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ def test_schema_for_args_adds_repo_project_config_options(tmp_path: Path) -> Non
assert "Demo Polish" in {option.name for option in schema.field_by_name("Initiative").options}


def test_base_project_schema_includes_tiny_size_before_small() -> None:
size_options = engine.BASE_PROJECT_SCHEMA.field_by_name("Size").options

assert [option.name for option in size_options] == ["T", "S", "M", "L"]
assert size_options[0].description == "Tiny, obvious change with no cross-module behavior."


def test_compare_schema_reports_missing_fields_wrong_types_and_missing_options() -> None:
fields = (
engine.ProjectField(field_id="status", name="Status", data_type="TEXT"),
Expand Down Expand Up @@ -253,6 +260,30 @@ def test_configuration_plan_preserves_extra_options_and_adds_required_options()
assert updated[1].option_id == "manual-later"


def test_configuration_plan_adds_tiny_size_to_existing_standard_size_field() -> None:
field = engine.ProjectField(
field_id="size",
name="Size",
data_type="SINGLE_SELECT",
options=(
engine.SelectOption(name="S", color="GREEN", description="Small", option_id="size-s"),
engine.SelectOption(name="M", color="YELLOW", description="Medium", option_id="size-m"),
engine.SelectOption(name="L", color="ORANGE", description="Large", option_id="size-l"),
),
)

actions = engine.configuration_plan(
project_exists=True,
fields=(field,),
schema=engine.ProjectSchema(fields=(engine.BASE_PROJECT_SCHEMA.field_by_name("Size"),)),
)

assert actions == (engine.ConfigureAction("update-field", "Size", "Add missing options: T."),)
updated = engine.merged_options(field, engine.BASE_PROJECT_SCHEMA.field_by_name("Size"))
assert [option.name for option in updated] == ["S", "M", "L", "T"]
assert [option.option_id for option in updated[:3]] == ["size-s", "size-m", "size-l"]


def test_resolve_issue_field_updates_returns_only_explicit_fields() -> None:
fields = (
engine.ProjectField(
Expand Down Expand Up @@ -317,6 +348,33 @@ def test_resolve_issue_field_updates_reports_missing_initiative_option() -> None
)


def test_resolve_issue_field_updates_reports_missing_size_option() -> None:
fields = (
engine.ProjectField(
field_id="size-field",
name="Size",
data_type="SINGLE_SELECT",
options=(
engine.SelectOption(name="S", color="GREEN", description="Small", option_id="size-s"),
engine.SelectOption(name="M", color="YELLOW", description="Medium", option_id="size-m"),
engine.SelectOption(name="L", color="ORANGE", description="Large", option_id="size-l"),
),
),
)

with pytest.raises(engine.ProjectUsageError) as excinfo:
engine.resolve_issue_field_updates(
fields,
{"size": "T"},
project_title="base",
)

assert str(excinfo.value) == (
"Size option 'T' was not found in Project 'base'. "
'Run `basectl gh project configure --project "base"` first.'
)


def test_doctor_command_fails_when_schema_is_incomplete(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ inspect the resolved command contract first.
| `basectl repo agent-guidance [path]` | Seed optional repo-local agent guidance files, optionally through a draft PR. | `--repo <owner/name>`, `--repo-name <name>`, `--default-branch <name>`, `--validation-command <cmd>`, `--pr`, `--dry-run` |
| `basectl repo installer-template [path]` | Write the maintained project installer starter script to a path, defaulting to `./install.sh`, optionally through a draft PR. | `--print`, `--repo <owner/name>`, `--pr`, `--dry-run` |
| `basectl gh issue list` | List GitHub issues through `gh`. | passes through `gh` options |
| `basectl gh issue create` | Create an issue with Base category conventions, assign it, and add repo Project metadata when the repo is known. Defaults to `--category enhancement` when omitted. | `--category <bug\|enhancement\|documentation\|ci\|security>`, `--title <title>`, `--body <body>`, `--repo <owner/name>`, `--project <title>`, `--project-owner <login>`, `--no-project` |
| `basectl gh issue create` | Create an issue with Base category conventions, assign it, and add repo Project metadata when the repo is known. Defaults to `--category enhancement` and Project `Size=S` when omitted. | `--category <bug\|enhancement\|documentation\|ci\|security>`, `--title <title>`, `--body <body>`, `--repo <owner/name>`, `--project <title>`, `--project-owner <login>`, `--size <T\|S\|M\|L>`, `--no-project` |
| `basectl gh issue start <number>` | Start issue-backed branch naming workflow. | `--category <category>`, `--title <title>` |
| `basectl gh pr create/status/checks/ready/merge` | Create and manage pull requests through Base's workflow wrapper. `pr create` auto-injects `Fixes #<issue>` from Base branch names unless `--no-fixes` is passed. | passes through `gh` options; `pr create` also accepts `--no-fixes` |
| `basectl gh branch stale` | Report stale local branches. | `--days <days>` |
Expand Down
Loading
Loading