feat: ship the coarse-Type fallback labels as a commented-out opt-in - #34
Conversation
…efaults Each default contradicted something the repository already documents. squash_merge_commit_title=PR_TITLE. AGENTS.md, CONTRIBUTING.md, pr-authoring and branch-and-commit all state the PR title becomes the commit message on main. GitHub's COMMIT_OR_PR_TITLE default makes that false whenever a PR has exactly one commit -- visible in this repo's own log, where #7 and #9 carry the (#N) suffix and #11, #13 and #15 do not. squash_merge_commit_message=PR_BODY. The COMMIT_MESSAGES default concatenates every branch commit message into the main commit body, and release-please deliberately parses that body for further Conventional Commits and BREAKING-CHANGE footers. Already live here: f98cbf9's body carries "* chore: trigger CI on release PR", harmless only because chore is release-please's hidden bucket. A branch commit reading "fix: wip" would have produced a phantom changelog entry or an unintended bump. allow_rebase_merge=false. The comment justifying rebase claimed release-please merges its own PR. It does not -- ADR-0002, the release-please workflow header and the release-management skill all require a human merge. Nor has rebase ever been used: the history has no merge commits, all eight merged PRs were squash-merged, and both release PRs cut their tags that way. allow_update_branch=true. The ruleset sets strict_required_status_checks_policy=false, so the "Update branch" button is not offered at all without it. Also corrects skills/release-management/SKILL.md, which instructed `gh pr merge --merge`. allow_merge_commit=false has been set since day one, so that command has always returned HTTP 405 -- the documented release procedure was broken. Its parenthetical was wrong too: release-please does not merge. Accepted cost: with PR_BODY an unmodified PR template lands verbatim in the main commit message. BLANK was rejected -- it drops Co-authored-by trailers and discards the RISK/rollback record. PR_BODY reduces the misparse surface but does not eliminate it; a Conventional-Commit-shaped line in a PR body is still parsed. Closes #24 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bootstrap did nothing about security settings -- grep for secret, scanning,
visibility, vulnerability or dependabot in the script returned nothing. Two
consequences. Converting a repo private to public grants ACCESS to secret
scanning but does not enable it, and push protection in particular must be
switched on explicitly, which is how a public repo ends up without it. And
.github/dependabot.yml asserts Dependabot alerts and security updates are
enabled while nothing verifies that.
The write path is bounded by cost, not by capability: the only settings this
phase ever enables are free by construction.
- Public repo: offers secret scanning and push protection, both free.
- Private or internal: never writes them, under any flag. There they need a
paid Advanced Security / Secret Protection seat, and a setup script must not
commit an adopter's account to a per-committer charge. It reports the state
and emits a MANUAL step.
- Dependabot alerts and automated security fixes: free everywhere, so offered
regardless of visibility.
- Repository visibility is never changed and never offered. Private to public
erases stars and watchers and publishes all Actions history -- a one-way
door, and the phase comment says so, because "detect whether the repo is
public" invites someone to add that prompt later.
Two states the GitHub UI blurs are kept distinct. Unreadable settings mean the
token lacks admin, not that the setting is off, so security_and_analysis
coming back null degrades to warn plus MANUAL rather than reporting "disabled".
And automated-security-fixes returns {"enabled":..,"paused":..}: enabled but
paused means no fix PR ever opens, so it is reported separately.
Verified read-only before writing the phase: gh api documents the
key[subkey]=value nested syntax, so the security_and_analysis PATCH stays a
normal run_or_dry call visible under --dry-run; /vulnerability-alerts returns
204 enabled and 404 disabled; /automated-security-fixes returns enabled and
paused.
Unlike phases 3/5/8 this phase runs several independent checks, so it
accumulates a result and calls record_phase once at the end -- the pattern
phase_issue_types uses, matching run_phase's one-record-per-exit-path
contract.
Phases 6/7/8 renumber to 7/8/9 across the script and docs. Because every read
runs for real under --dry-run, bootstrap --dry-run now doubles as a zero-risk
security audit.
Closes #26
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De-templating rewrote README, CHANGELOG and the release-please manifest and removed docs/template/, but never touched LICENSE -- grep for "license" in scripts/ returned nothing. So every adopted repository shipped MIT, copyright the template author. For an open-source adopter that is a wrong copyright line. For commissioned work it is an irrevocable written grant to the whole world of the right to use, modify, publish, distribute, sublicense and sell, made before payment, which removes the leverage the payment clause was built on. Four private client repos shipped that way. The obvious fix is itself a defect. Substantial portions of this template ship verbatim in every adopted repo, and MIT requires its copyright and permission notice to travel with them; rewriting LICENSE's copyright line deletes the only copy of that notice from the repository. So attribution is unconditional -- every answer that writes LICENSE also writes NOTICE, MIT-keep included. That, not the prompt, is the load-bearing part of this change. Three answers, no default. A bare Enter re-asks and a closed stdin defers, because confirm() treats closed stdin as "take the default", which here would mean silently shipping the template author's MIT -- the original bug with extra steps. --yes never writes a licence: it defers and files the decision as the first manual step, rendered above the others with a ! marker via a new MANUAL_URGENT list. --license mit|proprietary|defer answers non-interactively. Phase 9 runs before de-template and is not gated by --keep-template-docs: de-template returns early for three unrelated reasons, any of which would otherwise swallow the decision. It guards only LICENSE and NOTICE; adding LICENSE to phase 10's guard would make de-template skip itself on every run once phase 9 had written. LICENSE is regenerated from a seed rather than sed-patched, because holder names legitimately contain & and /. No docs/template/LICENSE.proprietary.example: under --yes the phase writes nothing and files a manual step, and phase 10 then removes docs/template -- deleting the example in the very run that told the adopter to read it. The body lives as a script constant, mirroring CHANGELOG_SEED, and in docs/setup/licensing.md which survives de-templating. Also adds scripts/check-license-marker.sh, wired into make check. If TEMPLATE_COPYRIGHT_* drifts from LICENSE the phase stops recognising its own licence and silently does nothing -- the original defect with no symptom. The check turns that into a red build, and no-ops in adopted repos. main is now guarded by a BASH_SOURCE test so the file can be sourced for its functions without running. Nothing in make verify covers this script; that guard is the only unit-test surface it has, and all nine licence paths were exercised through it. De-template renumbers 9 to 10. ADR-0004 records the decision and seven rejected alternatives. Closes #28 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All four workflows hardcoded runs-on: ubuntu-latest, so an adopter needing a different runner had to edit workflow YAML. That contradicted the template twice: AGENTS.md says customize the Makefile never the workflows, and design principle 5 promises workflow YAML stays untouched and upgradable. runs-on is the one adopter-facing property the Makefile physically cannot own, because GitHub resolves it when scheduling the job, before any make target exists. The rule was unsatisfiable for it. It also broke upgrading.md's advice to bulk cherry-pick .github/workflows/, since a forked runs-on line makes every template update a manual merge. One variable, not the four originally proposed: design principle 1, three of the four jobs are trivial and identical in shape, and every extra name is another thing to typo. A JSON array so multi-label self-hosted selection works. Unset reproduces today's behaviour exactly. The cost is real and was measured rather than assumed. A throwaway probe workflow using a separate variable name, so the required ci check was never at risk, was pushed with the value set to the tempting wrong `ubuntu-latest` -- a bare string instead of an array. Result: fromJSON fails during scheduling, the run completes with conclusion=failure and the message "This run likely failed because of a workflow file issue", ZERO jobs are created, and ZERO check runs are attached to the commit. Applied to ci.yml that means the only required check is never reported and the pull request cannot merge, with nothing in the Actions tab naming the variable. Recovery is `gh variable delete RUNNER_LABELS`. ci.yml's header now documents exactly that, alongside the two existing routes into the same trap. AGENTS.md is amended to name the exception rather than leave it as folklore, and hygiene rule 1 is amended to match so the skill does not contradict the shipped workflows. New docs/setup/runners.md covers the variable, the malformed-value failure and its recovery, the linux x86_64 bound imposed by install-ci-tools.sh, self-hosted runner caveats that were not written down anywhere (npm and pip installs escape INSTALL_DIR and run globally on the host every job), and why downsizing a runner to save money is usually a pessimisation on a repo shaped like this. ADR-0005 records the decision, and records the plain-string alternative as the closest call: it would remove the malformed-JSON footgun entirely at the cost of multi-label support. Closes #31 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PROJECT_FIELDS.md already documented that type:bug / type:feature labels are the fallback home for coarse Type when native issue types are unavailable, but labels.yml shipped no such labels -- an adopter had to invent the names, colours and descriptions and re-derive the single-home reasoning. They ship COMMENTED OUT rather than active, on mechanism rather than taste. A repo that has native issue types and an active type:bug label gives coarse Type two homes, the dual-write ADR-0003 forbids -- and that adopter cannot undo it by deleting the label, because the next bootstrap sync recreates anything labels.yml still declares. The adopter edits labels.yml either way; the edit should fall on whoever wants the label, not on whoever must not have it. It also keeps the labels-and-taxonomy budget of 14 literally true. Phase 2 gains enforcement in both directions, which "ship them active" cannot provide at all: on the 404 path it reports whether the fallback is in use and offers it; when native types ARE available and the fallback is also declared it reports a single-home violation. It reads the declared set via parse_labels_yml rather than the live repo, so --dry-run stays honest. It creates no labels and still cannot fail. Rejected alternative recorded in ADR-0006: creating the labels in phase 2 on the 404 path oscillates, because main() runs phase_labels first, so the next run prunes the undeclared label and phase 2 recreates it, forever. CORRECTION, found while testing this. The premise that native issue types are organization-only is no longer true. This repository is User-owned and repos/TzuH-Hsu/github-project-os/issue-types returns Bug/Feature/Task, created 2026-08-21 -- GitHub has rolled them out to personal accounts. Six places asserted "ORGANIZATION-only" / "requires an organization" / "this feature does not exist on personal accounts"; all six now say to check the endpoint rather than infer from account type, which is the condition that actually matters. The fallback is still needed -- the endpoint can still be unavailable -- but it is no longer a personal-account story. Verified both label states: as shipped the awk parser sees exactly 14 labels and the fallback reads as not declared; uncommented it sees 16, all colours valid 6-hex, and yamllint passes with the repo config. Both phase 2 branches were exercised, the org one via a stubbed gh since this account now has native types. Depends on #16 / #20: without the issue-labeler prefix-match fix, type:bug would delete itself on the next issue edit. Both must land before anyone uncomments. Closes #33 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ef03a77fae
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - The labels are applied **by hand**. `.github/workflows/issue-labeler.yml` | ||
| never adds or removes them: the coarse Type comes from the form's top-level | ||
| `type:` key, which is not part of the issue body the labeler parses. |
There was a problem hiding this comment.
Exclude fallback types from form-managed removals
When the fallback is enabled and an issue with a manually applied coarse-Type label is later edited, .github/workflows/issue-labeler.yml:119-138 classifies every type:* label as form-managed but only derives desired labels from the Task subtype field, so it removes type:bug or type:feature. The assertion here that the workflow never removes these labels is therefore false, and opted-in type metadata silently disappears; narrow the managed predicate to the subtype labels or explicitly exempt the fallback labels.
AGENTS.md reference: AGENTS.md:L9-L12
Useful? React with 👍 / 👎.
| coarse_type_fallback_declared() { | ||
| local labels_file=".github/labels.yml" | ||
| [ -f "$labels_file" ] || return 1 | ||
| parse_labels_yml "$labels_file" | cut -f1 | grep -qxF "type:bug" |
There was a problem hiding this comment.
If an adopter accidentally uncomments only type:feature, this predicate returns false, so a repository with native types passes without warning despite having two homes for Feature; conversely, uncommenting only type:bug makes phase 2 report that both fallback labels are in use even though Feature has no fallback home. Check the presence of each label and report a partial declaration instead of treating one label as proof of the pair.
AGENTS.md reference: AGENTS.md:L9-L12
Useful? React with 👍 / 👎.
| warn " - personal account: GitHub rolled these out to user accounts too, so check Settings -> Issue types before assuming you cannot have them" | ||
| warn " see .github/PROJECT_FIELDS.md for the documented fallback on personal accounts" | ||
| manual "Org repos: enable/verify native Bug/Feature/Task issue types in org settings. Personal accounts: the feature does not exist — see the 'Personal accounts' note in .github/PROJECT_FIELDS.md for the fallback" |
There was a problem hiding this comment.
Update the stale personal-account manual step
On every 404 or empty response, the new warning tells personal-account users that native issue types may be available in Settings, but the immediately following manual message still says the feature does not exist and points to a removed "Personal accounts" heading. This gives users contradictory setup instructions in the same phase; update that manual summary to match the new endpoint-based guidance and current heading.
Useful? React with 👍 / 👎.
1. An ampersand in the copyright holder corrupted LICENSE on bash >= 5.2.
${var//pat/repl} expands an unescaped & in the REPLACEMENT to the matched
text when patsub_replacement is on, which is the default from 5.2. A holder
of "Smith & Jones a/s" rendered as "Smith __HOLDER__ Jones a/s" while the
phase reported success. My earlier test missed it because macOS /bin/bash is
3.2, where the option does not exist -- and escaping as \& is itself literal
on 3.2, so no single expansion is correct on both. Replaced with subst_all,
a literal prefix/suffix-removal helper that never interprets the
replacement. Verified identical output on 3.2 and 5.3.
2. write_notice returned early whenever a NOTICE file existed, treating mere
existence as proof the upstream notice was present. An adopter with their
own NOTICE for other dependencies would have had LICENSE replaced while the
only copy of the template's MIT notice was silently dropped -- the exact
violation this phase exists to prevent. It now looks for the upstream
copyright line, appends when absent, and keeps existing content.
3. docs/setup/licensing.md's NOTICE block shipped literal __YEAR__ __HOLDER__
placeholders, so anyone following the documented manual path produced a
NOTICE with no attribution in it. It now carries the template's real
copyright line, with a sentence explaining that this one is deliberately not
the adopter's identity.
Fixing (1) introduced a regression that the doc-vs-output diff then caught:
command substitution strips trailing newlines, so LICENSE lost its final
newline and the NOTICE closing rule ran into "SOFTWARE.". Both fixed, the
constraint is documented on subst_all, and the doc block is now byte-identical
to what the script writes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pushed the previous commit before reading the markdownlint result -- my command chained on git add rather than gating on lint. CI would have caught it; that is not the same as checking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex review, and it is the sharpest finding in the stack. runners.md documented how to point RUNNER_LABELS at a self-hosted runner and listed only housekeeping caveats. But ci.yml triggers on pull_request, which on a public repository includes fork pull requests, and the job checks out the PR's own tree and runs make ci-tools and make ci-pr from it. Anyone on the internet could therefore execute their own Makefile and scripts on the adopter's machine, with its filesystem, network position and reachable credentials. Ephemeral cleanup does not help -- the damage happens during the job. The section now leads with the prohibition rather than burying it: on a public repo leave RUNNER_LABELS unset; the only safe shapes are making the repo private, or splitting the workflow so fork PRs stay on hosted runners. It also says why "require approval for first-time contributors" is not a fix -- approval is per-contributor, not per-diff. The operational caveats are demoted to a private-repository subsection, and ci.yml's header carries the warning at the point of risk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # docs/setup/bootstrap.md
Codex P1 addressedThe finding was correct for the branch state you reviewed: this chain was cut from That fix (#20, The only remaining Bringing The two P2s here (validating both fallback labels rather than just |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c792c57528
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ok "label fallback in use — coarse Type home is type:bug / type:feature (see .github/PROJECT_FIELDS.md)" | ||
| else | ||
| warn " label fallback NOT in use — this repo currently has no home for coarse Type" | ||
| manual "No coarse Type home. Either accept that (subtypes, priority and area still work), or uncomment the type:bug / type:feature block in .github/labels.yml and re-run bootstrap — see 'Personal accounts' in .github/PROJECT_FIELDS.md" |
There was a problem hiding this comment.
Distinguish lookup failures before recommending fallback labels
When this request fails for anything other than a confirmed 404/empty result—for example, rate limiting or a transient API/network error—lines 420–422 erase the failure and this new branch tells the adopter to uncomment the fallback labels. gh help exit-codes documents exit 1 as a command that “fails for any reason,” so a repository whose native types still exist can be misdiagnosed and guided into dual-writing Type. Preserve the error/status and offer the fallback only after a confirmed unavailable response; report other errors as lookup failures.
AGENTS.md reference: AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
1. An unreadable secret-scanning state was treated as disabled. security_and _analysis is only populated for callers with admin on the repo, so a token without it yields "public\tunknown\tunknown" -- a non-empty facts line that fell through to the else branch and prompted to enable, or PATCHed outright under --yes. That acts on a guess and reports a state never observed, which is the opposite of the unknown-is-not-disabled rule this phase was written around. There is now an explicit unknown branch that warns, emits a manual step, and writes nothing. 2. The Actions-permission troubleshooting entry still said phase 6 after the renumber moved it to 7, so the recovery text pointed at the Security prompt instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. LICENSE was replaced before NOTICE was known to be writable. If the NOTICE write then failed -- directory permissions, quota, I/O -- the repository was left with neither the template's original notice nor the promised attribution, a worse state than not running the phase at all. NOTICE is now written first and LICENSE is only touched once it succeeded. Verified by making NOTICE an unwritable path: the phase fails and LICENSE is still the template MIT, byte for byte. 2. The BASH_SOURCE guard covered main but not the top-level argument parser, so sourcing the file from a shell that had positional parameters consumed the caller's arguments and exit 1'd on the first one it did not recognise -- terminating the sourcing shell. Reproduced with `set -- unexpected-arg`. Both the parser and main now sit behind a bootstrap_is_main helper. Sourcing still applies set -euo pipefail to the caller; that is documented rather than silently changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. coarse_type_fallback_declared checked only type:bug, so a half-uncommented pair misreported in both directions: a repo with only type:feature passed the org single-home check silently, and a repo with only type:bug was told both labels were in use. Replaced with coarse_type_fallback_state, which echoes none / partial / both. The 404 branch now names the half-declared case and says to uncomment both or neither, since one label alone leaves the other coarse type homeless; the org branch treats partial as a violation too. 2. The 404 branch's manual step still said the feature does not exist on personal accounts and pointed at a "Personal accounts" heading that the same PR renamed. It contradicted the warning printed immediately above it, which already says to check Settings → Issue types. Rewritten to match, with the current heading. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex P2s addressed1. Partial declaration. Correct in both directions, and I had only considered one.
The half-declared message says to uncomment both or neither, since one label alone leaves the other coarse type with no home. 2. Contradictory manual step. Fixed. It still said the feature does not exist on personal accounts — contradicting the warning printed immediately above it — and pointed at the "Personal accounts" heading this same PR renamed. Both corrected. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fb9d4ab049
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # rolled out to personal accounts too, so check the endpoint rather than | ||
| # assuming from your account type -- bootstrap phase 2 checks it for you. | ||
| # | ||
| # Uncomment the two entries below ONLY on a personal-account repo where you |
There was a problem hiding this comment.
Gate the fallback on endpoint availability
On an organization-owned repository where the issue-types endpoint is unavailable—a case docs/setup/bootstrap.md:97-99 explicitly allows—this instruction forbids enabling the only fallback even though the ADR identifies endpoint availability, not account type, as the deciding condition. Such repositories are left without a coarse-Type home; permit the labels whenever the endpoint is 404/empty and forbid them only when native types are actually returned.
AGENTS.md reference: AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
…allback # Conflicts: # docs/adr/README.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d7d11d3e0e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if [ "$(coarse_type_fallback_state)" != "none" ]; then | ||
| violation=1 | ||
| warn "single-home violation: native issue types are available AND the type:bug/type:feature fallback is declared in .github/labels.yml" | ||
| warn " coarse Type now has two homes; they will drift (ADR-0003)" | ||
| manual "Re-comment or delete the type:bug / type:feature entries in .github/labels.yml and re-run bootstrap — with native issue types available, the native type is the single home for coarse Type" |
There was a problem hiding this comment.
Block fallback labels before syncing native-type repos
When an adopter uncomments the fallback on a repository where native types are available—the exact misconfiguration handled here—main() has already run phase 1 and created or updated type:bug and type:feature before this check executes. This branch only warns, so bootstrap leaves the repository with both metadata homes until the operator edits the file and runs it again; check availability before label sync or prevent those fallback labels from being created in this case.
AGENTS.md reference: AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
Summary
Ships
type:bug/type:featurein.github/labels.ymlcommented out, with phase 2 enforcing single-home in both directions. Also corrects a premise that has become false — see the correction section, which is the most important part of this PR.Related issue
Closes #33
Validation
make test(placeholder)Both label states, because a commented block is only safe if it parses as absent and correct when activated:
coarse_type_fallback_declaredtype:bugAll three phase-2 branches. The org branch is unreachable from this account's real state, so it was driven with a stubbed
gh:RESULT=ok, no warningsWARN single-home violation…+ manual step,RESULT=warnRisk / rollback
Checklist
*.local.mdfiles committedCorrection: "native issue types are organization-only" is no longer true
I found this while testing phase 2 and it changes the framing of the whole feature, so please read it before the rest.
This is a personal account, and it has native issue types. GitHub has rolled them out to user accounts since these docs were written. The original field report was accurate when it was filed — it just describes a GitHub limitation that has since been lifted.
Six places asserted the old behaviour as fact:
scripts/bootstrap.shphase 2.github/PROJECT_FIELDS.md.github/labels.yml(new block)docs/setup/bootstrap.md§2docs/setup/bootstrap.mdtroubleshootingdocs/adr/ADR-0006(new)All six now say to check the endpoint rather than infer from account type, which is the condition that actually determines behaviour. The
PROJECT_FIELDS.mdsection is renamed from "Personal accounts" to "When native issue types are unavailable" for the same reason.The fallback is still worth shipping — the endpoint can still be unavailable on some accounts and plans, and the single-home enforcement is valuable regardless — but it is no longer a personal-account story, and shipping docs asserting a falsehood would have been worse than not shipping the feature.
Why commented out, not active
The field report asked for active. Commented is correct on mechanism:
A repo with native issue types and an active
type:buglabel has two homes for coarse Type — the dual-write ADR-0003 forbids. And that adopter cannot undo it by deleting the label, because the nextscripts/bootstrap.shsync recreates anythinglabels.ymlstill declares. So the adopter editslabels.ymleither way; the edit should fall on whoever wants the label, not on whoever must not have it.This still answers the original complaint. The friction was "adopters must supply the colours, descriptions and the single-home explanation themselves" — the commented block ships all three, correctly placed, in the file adopters are already told to edit. Uncommenting is one edit, and it is the moment the adopter consciously picks one home.
It also keeps
skills/labels-and-taxonomyrule 3's "this template ships 14 labels" literally true — verified by the parser.Rejected alternative, on mechanism not principle
Having phase 2 create the labels on the 404 path oscillates:
main()runsphase_labelsbeforephase_issue_types, so the next run's phase 1 sees an undeclared label and prunes it (the default answer is yes), phase 2 recreates it, forever. Avoiding that needs a secondautorelease:*-style prune exclusion for a class of labels no file declares, and it breakslabels.yml's single-source-of-truth claim. Recorded in ADR-0006.Deliberately out of scope
Applying the labels automatically. The coarse Type comes from each form's top-level
type:key, which never appears incontext.payload.issue.body, andcontext.payload.issue.typeis null when the feature is unavailable. The only body-derived signal is heading-sniffing, which breaks the first time an adopter renames a form field — and adopters are explicitly told to customise these forms. Doing it properly needs a form-identity marker in all three forms plus labeler logic. Separate issue.ALLOWED_SUBTYPESis deliberately untouched: addingbug/featurethere would let a crafted### Subtypesection in an untrusted issue body minttype:bugon a Task, which is exactly what that hardcoded allowlist exists to prevent.