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 ] --title [--body ] [--repo ] [project options...]
+ basectl gh issue create [--category ] --title [--body ] [--repo ] [--size ] [project options...]
basectl gh issue start [--category ] [--title ]
basectl gh pr create [--no-fixes] [gh options...]
basectl gh pr status [gh options...]
@@ -34,6 +34,7 @@ Issue create project options:
--category Issue label category. Defaults to enhancement.
--project Project to update. Defaults to the repository name.
--project-owner Project owner. Defaults to the repository owner.
+ --size 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 ] --title [--body ] [--repo ] [project options...]
+ basectl gh issue create [--category ] --title [--body ] [--repo ] [--size ] [project options...]
basectl gh issue start [--category ] [--title ]
Purpose:
@@ -69,6 +70,7 @@ Issue create project options:
--category Issue label category. Defaults to enhancement.
--project Project to update. Defaults to the repository name.
--project-owner Project owner. Defaults to the repository owner.
+ --size 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 "* ]]
[[ "$output" == *"Issue create project options:"* ]]
+ [[ "$output" == *"--size "* ]]
[[ "$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 `, `--repo-name `, `--default-branch `, `--validation-command `, `--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 `, `--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 `, `--title `, `--body `, `--repo `, `--project `, `--project-owner `, `--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 `, `--title `, `--body `, `--repo `, `--project `, `--project-owner `, `--size `, `--no-project` |
| `basectl gh issue start ` | Start issue-backed branch naming workflow. | `--category `, `--title ` |
| `basectl gh pr create/status/checks/ready/merge` | Create and manage pull requests through Base's workflow wrapper. `pr create` auto-injects `Fixes #` 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 ` |
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 ` 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)