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
39 changes: 36 additions & 3 deletions goal/cli/governance_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,16 @@ def plan_adoption_command(catalog, catalog_sha256, target_root, target_revision)
raise click.exceptions.Exit(2)


def _governance_committed(target: Path) -> bool:
"""True when HEAD already contains an adopted governance manifest."""
result = subprocess.run(
["git", "-C", str(target), "cat-file", "-e", "HEAD:.governance/manifest.json"],
capture_output=True,
check=False,
)
return result.returncode == 0


@governance.command("adopt")
@click.option(
"--standard-repository",
Expand Down Expand Up @@ -678,6 +688,15 @@ def plan_adoption_command(catalog, catalog_sha256, target_root, target_revision)
"generator must record non-production provenance."
),
)
@click.option(
"--bootstrap-native-adoption",
is_flag=True,
help=(
"First adoption into a committed repository without governance: record "
"the installer receipt that the first --bootstrap-adoption-digest "
"ticket allocation requires."
),
)
@click.option(
"--pre-commit",
is_flag=True,
Expand All @@ -695,6 +714,7 @@ def adopt(
check,
upgrade,
allow_unpublished_for_testing,
bootstrap_native_adoption,
pre_commit,
ticket,
):
Expand All @@ -710,10 +730,12 @@ def adopt(
raise click.UsageError("--check and --upgrade are mutually exclusive")
if pre_commit and (not latest or not ticket):
raise click.UsageError("--pre-commit requires --latest and --ticket")
if pre_commit and (check or upgrade or allow_unpublished_for_testing):
if pre_commit and (
check or upgrade or allow_unpublished_for_testing or bootstrap_native_adoption
):
raise click.UsageError(
"--pre-commit cannot be combined with --check, --upgrade, or "
"--allow-unpublished-for-testing"
"--pre-commit cannot be combined with --check, --upgrade, "
"--allow-unpublished-for-testing, or --bootstrap-native-adoption"
)

target = target_root.resolve()
Expand Down Expand Up @@ -763,6 +785,9 @@ def adopt(
command.append("--upgrade")
if allow_unpublished_for_testing:
command.append("--allow-unpublished-for-testing")
if bootstrap_native_adoption:
command.append("--bootstrap-native-adoption")
fresh_repository = not check and not _governance_committed(target)

result = subprocess.run(command, capture_output=True, text=True, check=False)
if result.stdout:
Expand All @@ -771,6 +796,14 @@ def adopt(
click.echo(result.stderr, err=True, nl=False)
if result.returncode != 0:
raise click.exceptions.Exit(result.returncode)
if fresh_repository and not bootstrap_native_adoption:
click.echo(
"note: this repository has no committed governance; its first ticket "
"allocation needs the bootstrap receipt. Rerun with "
"--bootstrap-native-adoption, then allocate with "
"./project/new-ticket.sh --bootstrap-adoption-digest <dirtyDigest>.",
err=True,
)
if pre_commit:
click.echo(
f"prepared standard update {current_revision}..{source_revision} "
Expand Down
2 changes: 2 additions & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,6 @@ This file indexes governance tickets without taking ownership of
| **ticket-185** | [`README.md`](./ticket-185/README.md) | - | - | - | - | - |
| **ticket-186** | [`README.md`](./ticket-186/README.md) | - | - | - | - | - |
| **ticket-187** | [`README.md`](./ticket-187/README.md) | - | - | - | - | - |
| **ticket-188** | [`README.md`](./ticket-188/README.md) | - | - | - | - | - |
| **ticket-190** | [`README.md`](./ticket-190/README.md) | - | - | - | - | - |
<!-- AUTO:TICKET_INDEX:END -->
20 changes: 20 additions & 0 deletions project/ticket-191/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Ticket 191: Forward bootstrap-native-adoption from goal governance adopt (new-project 499)

- **ID**: ticket-191
- **Owner**: unresolved:human
- **Status**: IN_PROGRESS
- **Workflow state**: EDIT
- **Created**: 2026-10-20

## Goal and scope

To be completed from human-owned input.

## Acceptance criteria

- [ ] AC-01: Scope is approved by a human owner.

## Tracking boundary

This directory contains the minimal reviewed intent. Optional participant prose
and raw command logs are not required delivery output.
84 changes: 84 additions & 0 deletions project/ticket-191/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-191",
"summary": "Forward bootstrap-native-adoption from goal governance adopt (new-project 499)",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P1",
"origin": "requested"
},
"allowedPaths": [
"project/ticket-191/**",
"TODO.md",
"project/TICKETS.md",
"goal/cli/governance_cmd.py",
"tests/test_governance_cmd.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "297279a030a3182baa634c0b6bc7ea8ce34e48c2",
"targetBranch": "main",
"outcome": "goal governance adopt can perform the first adoption of a committed repository without governance so its first ticket can be allocated, and tells the operator when that route is needed.",
"nonGoals": [
"No automatic bootstrap without the explicit flag",
"No change to the new-project generator or allocator",
"No writer, review or publication authority from the receipt"
],
"complexity": "S",
"estimatedMinutes": 45,
"budgets": {
"maxImplementationFiles": 2,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 1,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Forward an explicit --bootstrap-native-adoption flag to the pinned create_adoption_lock.py, reject it with --pre-commit, and print a stderr note after a successful non-check adoption whose HEAD has no .governance/manifest.json.",
"components": [
{
"name": "governance-adopt-cli",
"paths": [
"goal/cli/governance_cmd.py",
"tests/test_governance_cmd.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [
"goal governance adopt --bootstrap-native-adoption"
],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Revert the commit; the generator flag remains usable directly."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python -m pytest tests/test_governance_cmd.py"
],
"evidence": "project/ticket-191/README.md"
},
{
"criterion": "AC-02",
"commands": [
"./project/governance-check.sh"
],
"evidence": "project/ticket-191/README.md"
}
]
}
}
103 changes: 103 additions & 0 deletions tests/test_governance_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ def make_standard(tmp_path):
parser.add_argument('--check', action='store_true')
parser.add_argument('--upgrade', action='store_true')
parser.add_argument('--allow-unpublished-for-testing', action='store_true')
parser.add_argument('--bootstrap-native-adoption', action='store_true')
args = parser.parse_args()
target = Path(args.target_root)
if args.bootstrap_native_adoption:
(target / '.bootstrap-native').write_text('explicit\\n', encoding='utf-8')
if args.allow_unpublished_for_testing:
(target / '.candidate-testing').write_text('explicit\\n', encoding='utf-8')
if args.check:
Expand Down Expand Up @@ -1035,3 +1038,103 @@ def test_rejects_check_with_upgrade(tmp_path):

assert result.exit_code == 2
assert "mutually exclusive" in result.output


def _adopt(standard, revision, target, *extra):
return CliRunner().invoke(
main,
[
"governance",
"adopt",
"--standard-repository",
str(standard),
"--source-revision",
revision,
"--target-root",
str(target),
*extra,
],
)


def _committed_repository(path, *, governance=False):
path.mkdir()
_git(path, "init", "--quiet")
_git(path, "config", "user.email", "goal-governance@example.invalid")
_git(path, "config", "user.name", "goal-governance-test")
(path / "README.md").write_text("fixture\n", encoding="utf-8")
if governance:
(path / ".governance").mkdir()
(path / ".governance" / "manifest.json").write_text("{}\n", encoding="utf-8")
_git(path, "add", ".")
_git(path, "commit", "--quiet", "-m", "fixture")
return path


def test_bootstrap_native_adoption_is_forwarded_without_note(tmp_path):
standard, revision = make_standard(tmp_path)
target = _committed_repository(tmp_path / "target")

result = _adopt(standard, revision, target, "--bootstrap-native-adoption")

assert result.exit_code == 0, result.output
assert (target / ".bootstrap-native").read_text(encoding="utf-8") == "explicit\n"
assert "--bootstrap-adoption-digest" not in result.output


def test_fresh_repository_without_bootstrap_flag_names_the_first_ticket_route(tmp_path):
standard, revision = make_standard(tmp_path)
target = _committed_repository(tmp_path / "target")

result = _adopt(standard, revision, target)

assert result.exit_code == 0, result.output
assert not (target / ".bootstrap-native").exists()
assert "--bootstrap-native-adoption" in result.output
assert "--bootstrap-adoption-digest" in result.output


def test_repository_with_committed_governance_gets_no_bootstrap_note(tmp_path):
standard, revision = make_standard(tmp_path)
target = _committed_repository(tmp_path / "target", governance=True)

result = _adopt(standard, revision, target, "--upgrade")

assert result.exit_code == 0, result.output
assert "--bootstrap-native-adoption" not in result.output


def test_check_gets_no_bootstrap_note(tmp_path):
standard, revision = make_standard(tmp_path)
target = _committed_repository(tmp_path / "target")

result = _adopt(standard, revision, target, "--check")

assert result.exit_code == 1
assert "--bootstrap-native-adoption" not in result.output


def test_pre_commit_rejects_bootstrap_native_adoption(tmp_path):
standard, _revision = make_standard(tmp_path)
target = _committed_repository(tmp_path / "target")

result = CliRunner().invoke(
main,
[
"governance",
"adopt",
"--standard-repository",
str(standard),
"--latest",
"--pre-commit",
"--ticket",
"ticket-001",
"--target-root",
str(target),
"--bootstrap-native-adoption",
],
)

assert result.exit_code == 2
assert "cannot be combined" in result.output
assert "--bootstrap-native-adoption" in result.output
Loading