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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ and Base versions are tracked in the repo-root `VERSION` file.
`basectl gh worktree prune`, with explicit PR-state classification and
retention details for open, closed-unmerged, and no-PR branches.

### Changed

- Relicensed Base prospectively under Apache-2.0 starting with v1.9.0 to reduce
adoption friction for companies with copyleft-averse license review. Existing
MIT and AGPL releases retain their original licenses.
- Made `basectl repo init` generate Apache-2.0 licenses by default for new
repositories.

### Fixed

- Batched branch and worktree GitHub PR-state verification through one
Expand Down
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ opinionated, testable, and useful as a local operating contract for deterministi
readiness and handoff across independent Git repositories. The durable product
loop is inventory -> prepare -> verify -> trust -> onboard -> hand off.

## License

Base is distributed under Apache-2.0 starting with v1.9.0. Earlier releases
retain the license stated in their release documentation. Unless a separate
written agreement says otherwise, contributions submitted for inclusion in Base
are accepted under the Apache-2.0 terms.

## AI-Assisted Development

Coding agents should follow [AGENTS.md](AGENTS.md). It points to the same
Expand Down
860 changes: 183 additions & 677 deletions LICENSE

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ basectl repo init example --repo basefoundry/example
```

This creates the local repository baseline: README, version, changelog,
contributing guide, AGPL-3.0-or-later license, `.gitignore`, `base_manifest.yaml`, a
contributing guide, Apache-2.0 license, `.gitignore`, `base_manifest.yaml`, a
`tests/validate.sh` contract, and a GitHub Actions workflow that runs it.
By default, `repo init` creates the repository under `workspace.root` from
`~/.base.d/config.yaml`; if that is not configured, it falls back to the parent
Expand All @@ -825,7 +825,7 @@ the current branch. Existing remotes are never implicitly pushed. Use
changes on a canonical issue-backed branch, push that branch to `origin`, and
open a pull request. Use `--no-configure` to skip the GitHub step, or rerun it
later with `basectl repo configure`. The generated license defaults to
`AGPL-3.0-or-later`; pass `--license Apache-2.0` for standalone package repos.
`Apache-2.0`, matching Base.
Real PR runs derive and verify the issue category;
offline `--pr --dry-run` previews also require `--category <name>`. Add
`--agent-ready` when a new baseline should also include `AGENTS.md` and
Expand Down Expand Up @@ -1997,7 +1997,8 @@ memory.

## License

Base is licensed under AGPL-3.0-or-later starting with v1.0.1.
Base is licensed under Apache-2.0 starting with v1.9.0.

Versions through v1.0.0 remain available under the MIT License as originally
Versions v1.0.1 through v1.8.0 remain available under AGPL-3.0-or-later, and
versions through v1.0.0 remain available under the MIT License as originally
published. See [LICENSE](LICENSE) for the current license terms.
123 changes: 20 additions & 103 deletions cli/bash/commands/basectl/subcommands/repo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ Options:
--release Seed the generic release contract and process documentation.
--language <csv> Add project language metadata; may be repeated.
--description <text> Repository description for generated README.
--license <SPDX> License for the generated repository (AGPL-3.0-or-later or Apache-2.0).
--copyright-holder <name> Copyright holder for generated AGPL license. Defaults to git config user.name.
--license <SPDX> License for the generated repository (Apache-2.0).
--private Create a private GitHub repository when needed. This is the default.
--public Create a public GitHub repository when needed.
--no-configure Skip GitHub configuration during repo init.
Expand Down Expand Up @@ -305,20 +304,6 @@ base_repo_default_description() {
printf 'Base-managed project %s.\n' "$name"
}

base_repo_default_copyright_holder() {
local holder=""

holder="$(git config --global user.name 2>/dev/null || true)"
if [[ -z "$holder" ]]; then
holder="$(id -un 2>/dev/null || true)"
fi
if [[ -z "$holder" ]]; then
holder="Unknown"
fi

printf '%s\n' "$holder"
}

base_repo_validate_name() {
local name="$1"

Expand Down Expand Up @@ -565,13 +550,6 @@ base_repo_clone_url() {
esac
}

base_repo_baseline_year() {
local year

printf -v year '%(%Y)T' -1 || return 1
printf '%s\n' "$year"
}

base_repo_create_directory() {
local target_dir="$1"

Expand Down Expand Up @@ -790,19 +768,9 @@ Closes #
EOF
}

base_repo_agpl_license_text() {
local source_license="$1"

awk '
/^[[:space:]]*GNU AFFERO GENERAL PUBLIC LICENSE$/ { found = 1 }
found { print }
END { if (!found) exit 1 }
' "$source_license"
}

base_repo_license_is_supported() {
case "$1" in
AGPL-3.0-or-later|Apache-2.0)
Apache-2.0)
return 0
;;
*)
Expand All @@ -812,64 +780,24 @@ base_repo_license_is_supported() {
}

base_repo_license_display() {
printf 'AGPL-3.0-or-later or Apache-2.0\n'
printf 'Apache-2.0\n'
}

base_repo_write_license() {
local canonical_license
local copyright_holder="$2"
local dry_run="$1"
local license_id="$3"
local root="$4"
local source_license="${BASE_HOME:-}/LICENSE"
local license_id="$2"
local root="$3"
local license_template="${BASE_HOME:-}/templates/licenses/Apache-2.0"
local year

case "$license_id" in
AGPL-3.0-or-later)
[[ -f "$source_license" ]] || {
base_std_log_error "Base AGPL license text '$source_license' was not found."
return 1
}

canonical_license="$(base_repo_agpl_license_text "$source_license")" || {
base_std_log_error "Base AGPL license text '$source_license' did not contain the canonical AGPL terms."
return 1
}

year="$(base_repo_baseline_year)"
{
cat <<EOF
Copyright (C) $year $copyright_holder

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
EOF
printf '\n'
printf '%s\n' "$canonical_license"
} | base_repo_write_stream "$dry_run" "$root/LICENSE"
;;
Apache-2.0)
[[ -f "$license_template" ]] || {
base_std_log_error "Apache-2.0 license template '$license_template' was not found."
return 1
}
base_repo_write_stream "$dry_run" "$root/LICENSE" < "$license_template"
;;
*)
base_std_log_error "Unsupported repository license '$license_id'. Expected: $(base_repo_license_display)"
return 1
;;
esac
[[ "$license_id" == "Apache-2.0" ]] || {
base_std_log_error "Unsupported repository license '$license_id'. Expected: $(base_repo_license_display)"
return 1
}
[[ -f "$license_template" ]] || {
base_std_log_error "Apache-2.0 license template '$license_template' was not found."
return 1
}
base_repo_write_stream "$dry_run" "$root/LICENSE" < "$license_template"
}

base_repo_write_gitignore() {
Expand Down Expand Up @@ -1204,13 +1132,12 @@ base_repo_write_project_support_files() {
}

base_repo_write_baseline() {
local copyright_holder="$4"
local description="$3"
local dry_run="$1"
local license_id="$6"
local license_id="$5"
local name="$2"
local root="$5"
local languages=("${@:7}")
local root="$4"
local languages=("${@:6}")
local status=0

if [[ "$dry_run" != "1" ]]; then
Expand All @@ -1223,7 +1150,7 @@ base_repo_write_baseline() {
base_repo_write_contributing "$dry_run" "$name" "$root" || status=1
base_repo_write_pull_request_template "$dry_run" "$root" || status=1
base_repo_write_project_config "$dry_run" "$root" || status=1
base_repo_write_license "$dry_run" "$copyright_holder" "$license_id" "$root" || status=1
base_repo_write_license "$dry_run" "$license_id" "$root" || status=1
base_repo_write_gitignore "$dry_run" "$root" || status=1
base_repo_write_manifest "$dry_run" "$name" "$root" "${languages[@]}" || status=1
base_repo_write_validate_script "$dry_run" "$root" || status=1
Expand Down Expand Up @@ -2120,7 +2047,6 @@ base_repo_init() {
local agent_ready=0
local configure=1
local baseline_change_status=0
local copyright_holder=""
local category=""
local create_pr=0
local default_branch=""
Expand All @@ -2130,7 +2056,7 @@ base_repo_init() {
local github_visibility="private"
local github_visibility_explicit=0
local issue=""
local license_id="AGPL-3.0-or-later"
local license_id="Apache-2.0"
local name=""
local path=""
local project_owner=""
Expand Down Expand Up @@ -2310,14 +2236,6 @@ base_repo_init() {
}
shift
;;
--copyright-holder)
[[ -n "${2:-}" ]] || {
base_repo_init_usage_error "Option '--copyright-holder' requires an argument."
return $?
}
copyright_holder="$2"
shift 2
;;
--private|--public)
requested_visibility="${1#--}"
if ((github_visibility_explicit)) && [[ "$github_visibility" != "$requested_visibility" ]]; then
Expand Down Expand Up @@ -2435,7 +2353,6 @@ base_repo_init() {
fi
[[ -n "$path" ]] || path="$(base_repo_default_target_path "$name")"
[[ -n "$description" ]] || description="$(base_repo_default_description "$name")"
[[ -n "$copyright_holder" ]] || copyright_holder="$(base_repo_default_copyright_holder)"
root="$(base_repo_target_path "$path")"
if [[ -n "$issue" ]] && ! base_github_issue_number_is_valid "$issue"; then
base_repo_init_usage_error "Option '--issue' must be a positive integer."
Expand Down Expand Up @@ -2517,7 +2434,7 @@ base_repo_init() {
return $?
fi

base_repo_write_baseline "$dry_run" "$name" "$description" "$copyright_holder" "$root" "$license_id" "${language_options[@]}" || return 1
base_repo_write_baseline "$dry_run" "$name" "$description" "$root" "$license_id" "${language_options[@]}" || return 1
if ((configure_release)); then
base_repo_configure_release "$dry_run" "$github_repo" "$root" || return 1
fi
Expand Down
2 changes: 1 addition & 1 deletion cli/bash/commands/basectl/tests/completions.bats
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ EOF
[[ "$output" == *"trust_allow_options=--workspace --manifest-sha256"* ]]
[[ "$output" == *"trust_revoke_options=--workspace"* ]]
[[ "$output" == *"repo_commands=init clone check configure agent-guidance installer-template"* ]]
[[ "$output" == *"repo_init_options=--path --repo --issue --category --pr --agent-ready --release --language --description --license --copyright-holder --private --public --no-configure --no-protect-default-branch --project --project-owner --project-schema --initiative-option --copy-project-fields-from --no-project --dry-run"* ]]
[[ "$output" == *"repo_init_options=--path --repo --issue --category --pr --agent-ready --release --language --description --license --private --public --no-configure --no-protect-default-branch --project --project-owner --project-schema --initiative-option --copy-project-fields-from --no-project --dry-run"* ]]
[[ "$output" == *"repo_clone_options=--owner --path --dry-run"* ]]
[[ "$output" == *"repo_check_options=--agent-guidance --agent-ready --release"* ]]
[[ "$output" == *"repo_configure_options=--repo --no-protect-default-branch --project --project-owner --project-schema --initiative-option --copy-project-fields-from --replace-project --no-project --release --dry-run"* ]]
Expand Down
1 change: 0 additions & 1 deletion cli/bash/commands/basectl/tests/help.bats
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,6 @@ load ./basectl_helpers.bash
[ "$status" -eq 0 ]
for flag in \
"--description <text>" \
"--copyright-holder <name>" \
"--project <title>" \
"--project-owner <login>" \
"--project-schema <schema>" \
Expand Down
70 changes: 3 additions & 67 deletions cli/bash/commands/basectl/tests/repo.bats
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,7 @@ EOF

[ "$status" -eq 2 ]
[[ "$output" == *"Unsupported repository license 'MIT'"* ]]
[[ "$output" == *"AGPL-3.0-or-later or Apache-2.0"* ]]
[[ "$output" == *"Apache-2.0"* ]]
[ ! -e "$repo_dir" ]
}

Expand Down Expand Up @@ -1445,7 +1445,8 @@ EOF
if grep -Fq "Demo Impact" "$repo_dir/.github/pull_request_template.md"; then
fail "pull request template should not include Demo Impact by default"
fi
grep -Fq "GNU Affero General Public License" "$repo_dir/LICENSE"
grep -Fq "Apache License" "$repo_dir/LICENSE"
grep -Fq "Version 2.0, January 2004" "$repo_dir/LICENSE"
run grep -F "Base - a workspace control plane" "$repo_dir/LICENSE"
[ "$status" -eq 1 ]

Expand Down Expand Up @@ -1538,71 +1539,6 @@ EOF
[[ "$output" == *"basectl repo init base-demo --path $repo_dir --repo <owner/base-demo>"* ]]
}

@test "basectl repo init defaults copyright holder to git user name" {
local repo_dir="$TEST_TMPDIR/git-owner"

cat > "$TEST_MOCKBIN/git" <<'EOF'
#!/usr/bin/env bash
if [[ "${1:-}" == "config" && "${2:-}" == "--global" && "${3:-}" == "user.name" ]]; then
printf 'Ada Lovelace\n'
exit 0
fi
exit 1
EOF
chmod +x "$TEST_MOCKBIN/git"

run env \
HOME="$TEST_HOME" \
PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \
"$BASE_REPO_ROOT/bin/basectl" repo init git-owner --path "$repo_dir" --no-configure

[ "$status" -eq 0 ]
grep -Fq "Copyright (C) $(date +%Y) Ada Lovelace" "$repo_dir/LICENSE"
grep -Fq "GNU Affero General Public License as published by" "$repo_dir/LICENSE"
}

@test "basectl repo init falls back to system username for copyright holder" {
local repo_dir="$TEST_TMPDIR/system-owner"
local username

cat > "$TEST_MOCKBIN/git" <<'EOF'
#!/usr/bin/env bash
exit 1
EOF
chmod +x "$TEST_MOCKBIN/git"
username="$(id -un)"

run env \
HOME="$TEST_HOME" \
PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \
"$BASE_REPO_ROOT/bin/basectl" repo init system-owner --path "$repo_dir" --no-configure

[ "$status" -eq 0 ]
grep -Fq "Copyright (C) $(date +%Y) $username" "$repo_dir/LICENSE"
grep -Fq "GNU Affero General Public License as published by" "$repo_dir/LICENSE"
}

@test "base_repo_baseline_year uses bash time formatting without date command" {
cat > "$TEST_MOCKBIN/date" <<'EOF'
#!/usr/bin/env bash
exit 1
EOF
chmod +x "$TEST_MOCKBIN/date"

run env \
HOME="$TEST_HOME" \
BASE_HOME="$BASE_REPO_ROOT" \
PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \
"$BASH" -c '
source "$BASE_HOME/base_init.sh"
source "$BASE_HOME/cli/bash/commands/basectl/subcommands/repo.sh"
base_repo_baseline_year
'

[ "$status" -eq 0 ]
[[ "$output" =~ ^[0-9]{4}$ ]]
}

@test "basectl repo init leaves existing files unchanged" {
local repo_dir="$TEST_TMPDIR/custom"

Expand Down
2 changes: 1 addition & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ daily project loop commands from the local checkout.

| Command | What it does | Important flags |
|---|---|---|
| `basectl repo init <name>` | Create a Base-managed repository baseline, including `.github/base-project.yml`, and optionally create/configure the GitHub repo. Use `--path .` for the current checkout. If `--repo` creates a new remote, init attaches `origin`, creates an initial commit, and pushes; existing remotes are never implicitly pushed. Use `--pr --issue <number>` for an explicit baseline PR. Real PR runs derive the issue category; offline `--pr --dry-run` also requires `--category <name>`. Use `--agent-ready` to seed `AGENTS.md` and `skills.md` with the baseline. Use repeatable `--language <csv>` values to seed normalized `project.languages` metadata; selecting `python` also writes `python.manager: uv`. | `--path <path>`, `--repo <owner/name>`, `--issue <number>`, `--category <name>`, `--language <csv>`, `--description <text>`, `--license <SPDX>`, `--copyright-holder <name>`, `--public`, `--private`, `--pr`, `--agent-ready`, `--project <title>`, `--project-owner <login>`, `--project-schema <schema>`, `--copy-project-fields-from <title>`, `--initiative-option <name>`, `--no-configure`, `--no-project`, `--no-protect-default-branch`, `--dry-run` |
| `basectl repo init <name>` | Create a Base-managed repository baseline, including `.github/base-project.yml`, and optionally create/configure the GitHub repo. Use `--path .` for the current checkout. If `--repo` creates a new remote, init attaches `origin`, creates an initial commit, and pushes; existing remotes are never implicitly pushed. Use `--pr --issue <number>` for an explicit baseline PR. Real PR runs derive the issue category; offline `--pr --dry-run` also requires `--category <name>`. Use `--agent-ready` to seed `AGENTS.md` and `skills.md` with the baseline. Use repeatable `--language <csv>` values to seed normalized `project.languages` metadata; selecting `python` also writes `python.manager: uv`. | `--path <path>`, `--repo <owner/name>`, `--issue <number>`, `--category <name>`, `--language <csv>`, `--description <text>`, `--license <SPDX>`, `--public`, `--private`, `--pr`, `--agent-ready`, `--project <title>`, `--project-owner <login>`, `--project-schema <schema>`, `--copy-project-fields-from <title>`, `--initiative-option <name>`, `--no-configure`, `--no-project`, `--no-protect-default-branch`, `--dry-run` |
| `basectl repo clone <name-or-owner/name>` | Clone one GitHub repository into the configured Base workspace, treating matching existing checkouts as already satisfied. | `--owner <owner>`, `--path <path>`, `--dry-run` |
| `basectl repo check [path]` | Verify the local repository baseline. Stable inspection JSON uses the shared v1 envelope. | `--agent-guidance`, `--agent-ready`, `--release`, `--format <text\|json>` |
| `basectl repo configure [path]` | Apply Base-managed GitHub repository settings, labels, default branch protection, non-default branch naming enforcement, the trusted issue/category branch policy workflow, and repo Project metadata. After a default-branch dispatch produces a recent trusted success, its GitHub-Actions-bound PR-head status becomes required. Reads `.github/base-project.yml` to seed options and fill missing issue defaults when present. | `--repo <owner/name>`, `--project <title>`, `--project-owner <login>`, `--project-schema <schema>`, `--copy-project-fields-from <title>`, `--initiative-option <name>`, `--replace-project`, `--no-project`, `--no-protect-default-branch`, `--dry-run` |
Expand Down
Loading
Loading