From b9c3d03c17b2dfe3ea0092259cccac4fc18e3c29 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah Date: Sat, 20 Jun 2026 17:57:09 -0700 Subject: [PATCH] Introduce tiny Project issue size --- .ai-context/COMMANDS.md | 4 +- .ai-context/WORKFLOWS.md | 4 ++ AGENTS.md | 3 + README.md | 5 +- cli/bash/commands/basectl/subcommands/gh.sh | 43 +++++++++++--- .../commands/basectl/tests/completions.bats | 1 + cli/bash/commands/basectl/tests/gh.bats | 55 ++++++++++++++++++ cli/python/base_github_projects/engine.py | 9 +-- .../base_github_projects/project_errors.py | 16 +++++ .../base_github_projects/project_model.py | 1 + .../base_github_projects/tests/test_engine.py | 58 +++++++++++++++++++ docs/command-reference.md | 2 +- docs/github-workflow.md | 16 ++++- docs/repo-baseline.md | 8 ++- lib/shell/completions/basectl_completion.sh | 2 +- 15 files changed, 205 insertions(+), 22 deletions(-) create mode 100644 cli/python/base_github_projects/project_errors.py diff --git a/.ai-context/COMMANDS.md b/.ai-context/COMMANDS.md index 4336aa9..b3dfdf2 100644 --- a/.ai-context/COMMANDS.md +++ b/.ai-context/COMMANDS.md @@ -44,7 +44,9 @@ the canonical current command list. - `basectl gh ` - 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 ` when the issue scope is clear; otherwise Project + metadata defaults to `Size=S`. - `basectl gh pr create` auto-injects `Fixes #` from Base branch names; pass `--no-fixes` to suppress that body injection. - `basectl gh project doctor --project ` - inspect Project metadata diff --git a/.ai-context/WORKFLOWS.md b/.ai-context/WORKFLOWS.md index ed17648..298b191 100644 --- a/.ai-context/WORKFLOWS.md +++ b/.ai-context/WORKFLOWS.md @@ -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`. diff --git a/AGENTS.md b/AGENTS.md index 9c683fc..89df2c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/README.md b/README.md index f299584..26b9c7a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cli/bash/commands/basectl/subcommands/gh.sh b/cli/bash/commands/basectl/subcommands/gh.sh index 0318c29..38b201b 100644 --- a/cli/bash/commands/basectl/subcommands/gh.sh +++ b/cli/bash/commands/basectl/subcommands/gh.sh @@ -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...] @@ -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: @@ -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: @@ -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. @@ -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 @@ -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="" @@ -422,6 +437,10 @@ base_gh_issue_create() { project_owner="${2:-}" shift ;; + --size) + project_size="${2:-}" + shift + ;; --no-project) configure_project=0 ;; @@ -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 @@ -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 } diff --git a/cli/bash/commands/basectl/tests/completions.bats b/cli/bash/commands/basectl/tests/completions.bats index ab3f499..dd07318 100644 --- a/cli/bash/commands/basectl/tests/completions.bats +++ b/cli/bash/commands/basectl/tests/completions.bats @@ -266,5 +266,6 @@ EOF [[ "$output" == *"--category"* ]] [[ "$output" == *"--title"* ]] [[ "$output" == *"--body"* ]] + [[ "$output" == *"--size"* ]] [[ "$output" != *"--type"* ]] } diff --git a/cli/bash/commands/basectl/tests/gh.bats b/cli/bash/commands/basectl/tests/gh.bats index eb3bd04..bf414ce 100644 --- a/cli/bash/commands/basectl/tests/gh.bats +++ b/cli/bash/commands/basectl/tests/gh.bats @@ -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"* ]] @@ -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 diff --git a/cli/python/base_github_projects/engine.py b/cli/python/base_github_projects/engine.py index b50ad1c..3781aea 100644 --- a/cli/python/base_github_projects/engine.py +++ b/cli/python/base_github_projects/engine.py @@ -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): @@ -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) diff --git a/cli/python/base_github_projects/project_errors.py b/cli/python/base_github_projects/project_errors.py new file mode 100644 index 0000000..d6df8bc --- /dev/null +++ b/cli/python/base_github_projects/project_errors.py @@ -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}'." diff --git a/cli/python/base_github_projects/project_model.py b/cli/python/base_github_projects/project_model.py index 11e545e..6bf7f2c 100644 --- a/cli/python/base_github_projects/project_model.py +++ b/cli/python/base_github_projects/project_model.py @@ -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."), diff --git a/cli/python/base_github_projects/tests/test_engine.py b/cli/python/base_github_projects/tests/test_engine.py index 2906efb..caf73ed 100644 --- a/cli/python/base_github_projects/tests/test_engine.py +++ b/cli/python/base_github_projects/tests/test_engine.py @@ -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"), @@ -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( @@ -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: diff --git a/docs/command-reference.md b/docs/command-reference.md index 49d30eb..3af527b 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -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>` | diff --git a/docs/github-workflow.md b/docs/github-workflow.md index 228e438..4d23119 100644 --- a/docs/github-workflow.md +++ b/docs/github-workflow.md @@ -46,10 +46,22 @@ Base roadmap fields: - `Priority`: `P0`, `P1`, `P2`, `P3` - `Area`: `CLI`, `Setup`, `Workspace`, `Manifest`, `Runtime`, `Shell`, `Python`, `Docs`, `CI`, `Packaging`, `Security`, `Product` -- `Size`: `S`, `M`, `L` +- `Size`: `T`, `S`, `M`, `L` - `Initiative`: `BanyanLabs Dogfood`, `Workspace Handling`, `pyproject/uv`, `v1.0 Readiness`, `Adoption Polish` +Use the smallest accurate `Size` when creating or triaging an issue: + +- `T`: tiny, obvious change; usually one file or one Project metadata action, + with no design decision or cross-module behavior. +- `S`: small, focused change that may still need tests, docs, or a few files. +- `M`: medium change with multiple files or interactions. +- `L`: large change that should be split if possible. + +Automation defaults to `S` when no explicit size is supplied. Agents creating +issues should pass an explicit size when the scope is already clear; use `S` +when the issue still needs normal triage. + Keep the issue's `Status` field aligned with the implementation train: - Set `Status` to `In Progress` before creating the implementation branch or @@ -416,7 +428,7 @@ Recommended fields: - `Status`: Triage, Backlog, Ready, In Progress, In Review, Done - `Priority`: P0, P1, P2, P3 - `Area`: repo-specific options declared in `.github/base-project.yml` -- `Size`: S, M, L +- `Size`: T, S, M, L - `Initiative`: repo-specific options declared in `.github/base-project.yml` Keep `Status`, `Priority`, and `Size` standardized across repos. Keep `Area` diff --git a/docs/repo-baseline.md b/docs/repo-baseline.md index 9b0aec8..ef1e3e1 100644 --- a/docs/repo-baseline.md +++ b/docs/repo-baseline.md @@ -339,11 +339,17 @@ the repo Project: - `Priority`: `P0`, `P1`, `P2`, `P3` - `Area`: `CLI`, `Setup`, `Workspace`, `Manifest`, `Runtime`, `Shell`, `Python`, `Docs`, `CI`, `Packaging`, `Security`, `Product` -- `Size`: `S`, `M`, `L` +- `Size`: `T`, `S`, `M`, `L` - `Initiative`: `BanyanLabs Dogfood`, `Workspace Handling`, `pyproject/uv`, `v1.0 Readiness`, `Adoption Polish`, plus values passed with `--initiative-option` +`T` means a tiny, obvious issue with no design decision or cross-module +behavior. `S` remains the generated and fallback default because new issues are +not always fully scoped at creation time. `basectl repo configure` and +`basectl gh project configure` add missing shared Project options +additively; existing item values are preserved. + `--copy-project-fields-from <title>` copies these single-select fields when the source Project item has a value and the target repo Project item does not: `Status`, `Priority`, `Area`, `Initiative`, and `Size`. Values are skipped and diff --git a/lib/shell/completions/basectl_completion.sh b/lib/shell/completions/basectl_completion.sh index 9d806e5..7f327a4 100644 --- a/lib/shell/completions/basectl_completion.sh +++ b/lib/shell/completions/basectl_completion.sh @@ -197,7 +197,7 @@ _base_basectl_completion() { if ((COMP_CWORD == 3)); then _base_basectl_completion_compgen "list create start" "$cur" else - _base_basectl_completion_compgen "--category --title --body -h --help" "$cur" + _base_basectl_completion_compgen "--category --title --body --repo --project --project-owner --size --no-project -h --help" "$cur" fi ;; pr)