diff --git a/.gitattributes b/.gitattributes index 182c875..583ef24 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,6 @@ *.webp binary # Generated SVGs are hashed into docs/images/.showcase.json; keep their bytes identical on every OS *.svg text eol=lf +# Docs and the changelog stay LF: a CRLF commit rewrites every line and conflicts every branch (CHANGELOG Unreleased) +CHANGELOG.md text eol=lf +references/*.md text eol=lf diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index f161f59..d264db7 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -24,7 +24,7 @@ jobs: - name: Install Python quality dependencies run: | python -m pip install --upgrade pip - python -m pip install -r requirements.txt ruff==0.17.2 pyyaml==6.0.3 pytest==9.1.1 + python -m pip install -r requirements.txt ruff==0.17.2 pyyaml==6.0.3 pytest==9.1.1 jsonschema==4.25.1 - name: Install diagram and type-check tools run: npm install -g @mermaid-js/mermaid-cli pyright - name: Verify the bundled OFL fonts diff --git a/.github/workflows/test-portability.yml b/.github/workflows/test-portability.yml index d7d9482..b79a04c 100644 --- a/.github/workflows/test-portability.yml +++ b/.github/workflows/test-portability.yml @@ -21,6 +21,6 @@ jobs: with: python-version: '3.12' - name: Install test dependencies - run: python -m pip install -r requirements.txt pytest==9.1.1 + run: python -m pip install -r requirements.txt pytest==9.1.1 jsonschema==4.25.1 - name: Run the full test suite run: python -m pytest -q diff --git a/CHANGELOG.md b/CHANGELOG.md index cd69259..b89b089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo ### Added +- T3 captured Org option identity (issue 23, round 5): a captured Org option is resolved by its description (`T3: `) and by its option name independently, so an unresolved or empty description no longer hides an option name that is a known T3 org's display name (the option is checked for a record and held with `t3-signoff::` without one), and the Org-options mutation of a retained option carries its `signoff_id` (merged with existing ids) even when the captured label differs from the current names. Regressions in `tests/test_t3_emission_scope.py`. +- T3 sign-off at the profile destination and for captured Org option provenance (issue 23, round 4): `org profile --allow-t3` now also requires the org's `.github` repository sign-off, so a `.github`-scoped withdrawal holds the profile (`t3-signoff::OWNER/.github`, nothing built or written, existing README and data.json untouched) while the individual repository checks still apply inside it, and the README job of a copied Estate board carries the granting ids of every retained captured T3 Org option (merged with ids already on the job) instead of rediscovering them from rendered text. Regressions in `tests/test_t3_emission_scope.py`. +- `project copy-plan --allow-t3` authorizes the `repo:` qualifiers of each proposed view filter before emitting its mutation (issue 23, adjudication t23-adjudication-view-filter-3100): a filter naming a denied or withdrawn repository is held (`view-filter-held` plus `t3-signoff:`), a permitted one carries the covering `signoff_id`s, and a non-T3 owner's filter is unchanged. Negative, empty and wildcard `repo:` values are ignored. Qualifiers inside grouping parentheses, such as `(repo:Owner/x) workstream:Research`, are authorized and stamped too. Regressions in `tests/test_t3_view_filter_repos.py`. +- T3 sign-off stored scopes, held pin order and proposal stamping (issue 23, round 13): stored `repos` entries of grants and withdrawals are canonicalized like the requested repository (`site/`, ` site`, `site.git` equal `site`, `*` kept) in `evaluate`, `_covers` and the withdrawal match, and an entry that cannot be canonicalized makes the record malformed (`t3-signoff:invalid`, fails closed for a withdrawal, never authorizes for a grant); a non-boolean `approved` on a scoped record fails closed for that scope only. `org pins --allow-t3` holds the exact-set order job of a target that has a pin held for sign-off (`pins-order-held::t3-signoff`) while the authorized pin jobs remain, so a held pin is not unticked. Writer-shaped `proposals` (the `content actions` workflow-toggle) carry `signoff_id` in the report and on stdout and stay out of the `--jobs` queue. Tests: tests/test_t3_signoff_withdrawal.py, tests/test_t3_signoff_require.py, tests/test_t3_signoff_proposals.py. +- T3 sign-off repository spelling canonicalization (issue 23, adjudication t23-adjudication-repo-canonicalization-50f): the requested repository is canonicalized (whitespace, trailing `/`, `.git`) before the `*` shortcut in `signoff.evaluate()`/`_covers()`, so a per-repository withdrawal can no longer be bypassed by `x/`, ` x`, `x.git`; an empty or invalid name fails closed (`invalid:repo:`). `project copy-plan --repos` entries are stripped and validated (a malformed entry is a usage refusal), README and link entries are normalised once (a bare name on an org board is `/`, owners stripped and resolved, unresolvable owners held), and without `--allow-t3` a T3 repository on a non-T3 board README is omitted and logged `t3-signoff:readme:/`. `references/t3-signoff.md` documents the spelling rules and maps the record fields `repos`, `approved_on` and `expires` to the issue #24 wording. Tests: tests/test_t3_signoff_withdrawal.py, tests/test_t3_cli_copy_plan_consent.py, tests/test_t3_emission_scope.py. +- Default-on T3 sign-off enforcement (issue 23, tasks 24-27): `--allow-t3` is honoured only while an approved, unexpired record covers the owner (and the repository when the record is scoped). The record file is the private estate file `/decisions/t3-signoffs.yaml` (`signoff.default_record_path()`; estate root from `SHOWCASE_ESTATE_ROOT`, else the skill's parent bundle); `--t3-signoff-file PATH` (env `SHOWCASE_T3_SIGNOFF_FILE`) overrides it, resolved in the order flag, env, default. When the resolved file does not exist, or no estate root resolves, the command exits 2 with `t3-signoff:no-file` naming the path; the tool never creates the file and never falls back to trusting the flag. There is no opt-out: no flag or environment variable turns the check off. `expires` is optional (absent means no expiry; null or unparseable is still invalid). An `approved: false` record dated on or after a grant (the same day counts), for the same owner, vetoes the grant for every repository the withdrawal covers (`*` covers all), so a co-owner can withdraw one repository from a `*` or multi-repository grant (`t3-signoff:revoked`); a malformed withdrawal that could cover the request fails closed with `t3-signoff:invalid` instead of leaving the grant standing; a `repos` that is not `"*"`, a name or a non-empty list is `t3-signoff:invalid`. A bare `--allow-t3` therefore no longer works without a record: this is a behaviour change for every T3 run. A repository given as `OWNER/NAME` is covered by a record only when that owner is the record's own org (only `*` covers another account's repository), and `project estate-plan` credits a job to a T3 org only by its login, its `T3: login` description or an unambiguous display name. Record ids must be unique within the file (a repeat refuses `t3-signoff:invalid`). `doctor` adds the trailing `signoff_id` column only when `--allow-t3` is passed. Transfer-target holds (an approved pending transfer into a T3 org) apply only when `--allow-t3` is passed, so T1/T2 org output is unchanged by default. A record whose `approved_on` is later than today, or whose `expires` is earlier than its `approved_on`, is invalid (`t3-signoff:invalid`) and grants nothing; a malformed `repos` value is always reported as `t3-signoff:invalid`, never `t3-signoff:scope`, even beside a valid grant for an unrelated repository. A withdrawal and a grant match when repositories are written as a bare name or `OWNER/NAME` under the owner or any of its aliases. `project estate-plan --allow-t3` records `t3-signoff::` in the plan's `logs` for every T3 org it omits for lack of a valid record. Writer jobs for a signed-off T3 owner carry `signoff_id` only (`approval_id` is never set from a sign-off: it names the decisions/APPROVED_ACTIONS.yaml row), including the star-List jobs `org pins` plans on the personal account; an item job that adds a T3 repository to a T3 org's List keeps both grants in `signoff_ids` (the favorite's record, the List org's record and any newly added T3 destination List's record). Change records carry `signoff_id`, and a T3 association (`topics --also-in`, `adopt`'s `also_in`, `project estate-plan`) carries that org's record. A record file whose envelope breaks the schema (a top-level list, an extra top-level key, a non-object entry) authorizes nothing. Excluded and HOLD owners stay refused. A `*` grant with a later per-repository withdrawal no longer lets `org profile` name, or `org pins` pin, the withdrawn repository (it is omitted or held, log `t3-signoff::/`, while the org's other repositories keep the grant), and `project estate-plan --allow-t3` holds the Org option update and README when the board's already captured Org options name a T3 org without a valid record (no option id is deleted). Issue #26 asks for `approval_id` on jobs; jobs carry `signoff_id` instead, because `approval_id` names the APPROVED_ACTIONS.yaml row (recorded in references/t3-signoff.md). +- `project copy-plan --allow-t3` builds the §13.9 step 7 settings job (default repository, auto-add) from the sign-off-authorized repositories only: a withdrawn or unauthorized repository is never chosen, the job is held with `settings-held:` when none remains, and the job carries the `signoff_id` of the repository it names (issue 23). +- `project copy-plan --allow-t3` decides the repository sign-off once, before the README and browser jobs (issue 23): a withdrawn or unauthorized repository (listed in `--repos` or only captured in the board's README) is named by no README link, and the README job carries the granting ids of the links it keeps (a repository-specific regrant, not the cached wildcard). The `-workflows` browser job omits its auto-add step (log `workflows-held:auto-add`) when no repository is authorized, otherwise names the authorized repository and carries its granting `signoff_id`. Job order and ids are unchanged; standalone `project readme` is unchanged. +- `project copy-plan --allow-t3` authorizes every README repository on its own resolved owner and name, not on the board owner's tier (issue 23): a T3 owner's repository named on a non-T3 board, whether listed in `--repos` or only captured in the board's repositories, needs a valid record covering that repository. A missing or withdrawn one is omitted from the README and logs `t3-signoff::OWNER/REPO` and `readme-held:OWNER/REPO`; a permitted one stays and its granting `signoff_id` (a repository-specific regrant, not the cached wildcard) is carried on the README job. No link job is created for a captured repository, other owners' repositories and runs without `--allow-t3` are unchanged, and standalone `project readme` is unchanged. +- Also lands #28 (context-local sign-off, ContextVar) and #31 (global --allow-t3 docs), merged from epic/E1 (see Fixed below). +- T3 sign-off owner and emission scope (issue 23): record owners are matched by one normalisation (all surrounding whitespace stripped, then case-folded) in `evaluate`, withdrawals and `record_for`, so a padded owner can no longer escape its own withdrawal; `project copy-plan --allow-t3` checks every repository link against the sign-off for that repository (a withdrawn link is not emitted and logs `t3-signoff::` and `link-held`, a permitted link carries its own `signoff_id`); and captured T3 Org options are held (and their option ids kept) whether or not the org is still in `registry.orgs`, with authorized retained options stamped with their sign-off id on the Org-update and README jobs. +- T3 sign-off association ids (issue 23): sign-off ids already on a job are merged, not replaced, when an Org or association grant is stamped, and direct `project copy-plan --role estate` stamps captured T3 Org options exactly as `project estate-plan` does. Tests: tests/test_t3_signoff_withdrawal.py. +- T3 copy-plan consent at CLI level and for captured repositories (issue 23): `project copy-plan --allow-t3` without `--repos` now uses the captured same-owner repositories, each authorized on its own resolved owner and name, for the workflows auto-add and settings default repository (a regrant names the regranted repository with its id; a populated capture that is all withdrawn names no repository and holds both; an absent capture keeps the generic control; no link job is created for a captured repository). Tests: tests/test_t3_cli_copy_plan_consent.py runs the real `scripts/showcase.py project copy-plan` (README omits the withdrawn repository, keeps the kept one, carries the regrant ids; no record refuses the run) and the same-owner captured README, workflows and settings jobs. - This repository now bundles its own `assets/icon.png` (the normalized Codex-generated icon, provenance in `assets/PROMPTS.md`) and persists `chips.key` (`Claude + Codex`) and `chips.attrs` (`Python`, `Offline`) in `.github/showcase.yml`, so the banner and the card both take the same configured chips through `chips.derive` and the topic-derived `Claude · Codex` runtime chip is not repeated; a synthetic no-network regression test covers it. The README banner and card are regenerated from the committed configuration and icon (`banner.png` and `card.svg` are byte-identical to the visually reviewed render), and the icon provenance records the exact prompt, tool and hashes (#132). - The pre-#61 override compatibility test now uses a minimal synthetic legacy art-direction fixture (`tests/fixtures/content/ART_DIRECTION.synthetic-legacy.md`, placeholder names and neutral colours, no production brand data) instead of a copy of the previous bundled template; the original stays in git history. - `content icon prompt` takes `--seed N` to request an image seed. The prompt states it only when asked, and the markdown provenance line `Requested: 1 image; seed: ...` records `N` or `not supported` (#63). An invalid or missing glyph `size_class` stays `needs-writer` and the result (`note`, `invalid`) lists the allowed values WIDE, TALL, SQUARE and DIAGONAL with a one-line meaning each; the values are listed in `SKILL.md`, `references/content.md` and `references/art-direction.md` (#62). @@ -16,6 +29,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo - `content icon check` and `content icon normalize` take `--min-contrast` (and the family key `icon_min_contrast`, range 1 to 21) to set the contrast floor of the icon ink on the card. Precedence is the flag, then the family key, then the default 3.0, so existing results do not change. 4.5:1 is recommended for the card plate but is not the default. - `content icon check` and `content icon normalize` take `--strict` (and the family key `icon_strict_family_ink: true`, off by default) to fail instead of warn when the dominant icon ink is not a family ink. Without a family palette the family-ink check is still not emitted. - `org labels ORG --repos-from-gh` plans labels across accessible repositories without the private estate registry, with paginated read-only GitHub capture, optional verified local checkouts, unchanged reference guards, and a documented org-default-forms-first workflow. Registry-backed plans still require the org registry row, and checkout repository names match GitHub case-insensitively, including resolved pending renames. Registry-free runs also isolate lazy brand lookups from estate decision files, and held reference rewrites retain their file/line guidance with an incomplete result. Workflow/labeler/release/Renovate references remain final skips rather than unresolved input requests. Missing Windows device-named checkouts fall back to captured metadata instead of aborting the org plan. Pending transfers check the repository's current owner's default issue forms and hold renames when that evidence is unavailable. Sparse or partially missing checkouts retain reference holds based on captured paths and the Git index, while unrelated omitted files remain allowed. +- Global `--allow-t3` (before the command) signs off a T3 co-owned repository for one invocation across every repository command, generator, `lint` and the `org`, `project` and `content` groups, not only `meta`. The default still refuses T3 owners (exit 2, `t3-signoff`), except repository `doctor` (always admits T3 owners; R7 is evaluated normally only with the flag and a valid covering record, otherwise it stays `hold`; it still refuses excluded and HOLD owners), local `meta` normalization and `fingerprint`, which never refuse a T3 owner; excluded and HOLD owners stay refused. Pick Your AI also gets its own palette, mark and attribution (PR #20). - `references/art-direction.md` documents the Codex image-generation recipe for the icon pipeline (the `codex exec` command, reading the image path from the run output, and the retry and attempt limits). Image-folder and sandbox notes are labelled as observed runtime behaviour, not OpenAI-documented, and the exact returned path is the rule. Each distinct returned path is one candidate, and the attempt cap is per candidate slot (four slots, 3 empty attempts each); unreadable-only output counts as an empty attempt and ends in `art-fail` after 3. Documentation and static wording tests only; no behaviour change. ### Changed @@ -25,6 +39,7 @@ All notable changes to `github-showcase-skill` are recorded here. Versions follo ### Fixed +- T3 sign-off (`--allow-t3`) is now context-local (contextvars) so concurrent or embedded calls cannot leak sign-off to each other; CLI behaviour unchanged. - Custom transparent icon art (`--icon`, `showcase.yml` `icon`, `assets/icon.png`), including PNG mode P with transparency and RGBA, is fitted by its alpha bounds in the icon box in the `iconbox` compositor, so transparent margins no longer shrink it. The raw and normalized PNG are unchanged; anything under `assets/brand` is bundled artwork protected by its resolved path, not by the selection route, so `--icon` or `showcase.yml` `icon` pointing at `logo-dark.png` or `logo-dark-full.png` is no longer cropped (2400x1198 and 1200x599 keep their sizes); golden plate geometry, opaque or full-bleed icons, pixel-art sizing and fully blank transparency are unchanged. With `icon_pixel_art` set, alpha fit never applies, including an unrecognized native grid, which keeps the retained image size and placement (#130; reuses the #126 normalizer, no new normalizer). - `readme --bootstrap --write` no longer regenerates a marked footer that holds authored content. When the edited-template upgrade is refused by the never-drop-a-line check and the marked footer carries a partner or foreign image, the command now exits with a check failure (`readme-shrink`) and leaves `README.md` unchanged instead of falling back to a header refresh that rewrote the footer; a plain old footer copy still takes the fallback. The same refusal now applies in every bootstrap state (pristine and unmarked as well as edited), checked before any write, so a pristine or unmarked template can no longer silently replace a partner or foreign footer image. - Fork (G) and backup (H) READMEs migrate an unmarked trailing footer whose logo is a configured own-site historical source (`footer.legacy_logo_urls` or `docs/images/logo.svg`) to the single current footer; partner, foreign and look-alike images stay authored and are reported as no allowance. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc88ffc..00f6402 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ Use synthetic fixtures and new output paths instead of overwriting committed pre The full gate for this repository adds the test suite and the showcase quality gates to the package baseline: ```bash -python -m pip install -r requirements.txt ruff==0.17.2 pytest==9.1.1 +python -m pip install -r requirements.txt ruff==0.17.2 pytest==9.1.1 jsonschema==4.25.1 python -m pytest -q python scripts/validate_quality.py ``` diff --git a/ONBOARDING.md b/ONBOARDING.md index 2319c70..3e94964 100644 --- a/ONBOARDING.md +++ b/ONBOARDING.md @@ -68,7 +68,7 @@ For a multi-skill handoff, preserve the source facts, artifact paths, `logs[]` c ## Safety and permissions -The documented generation workflow is local. Installation may contact GitHub and package indexes. `meta --from-gh` and the opt-in `org labels ORG --repos-from-gh` mode make read-only `gh api` GETs. `fonts --fetch` downloads the pinned fonts. Org-wide label planning emits proposed jobs for review; it never applies them. Generation does not push commits, change descriptions, topics, labels or settings, publish documents, grant approval or install background services. Review the boundaries in [SKILL.md](SKILL.md) before using real repositories. +The documented generation workflow is local. Installation may contact GitHub and package indexes. `meta --from-gh` and the opt-in `org labels ORG --repos-from-gh` mode make read-only `gh api` GETs. `fonts --fetch` downloads the pinned fonts. Org-wide label planning emits proposed jobs for review; it never applies them. Generation does not push commits, change descriptions, topics, labels or settings, publish documents, grant approval or install background services. T3 (co-owned) owners are refused unless `--allow-t3` is passed for one approved invocation; the claim is checked by default against a sign-off record in the private estate bundle (`decisions/t3-signoffs.yaml`, or `--t3-signoff-file PATH`; there is no opt-out) ([references/t3-signoff.md](references/t3-signoff.md)). Keep real sign-off records in the private estate bundle; never copy them into a repository. Review the boundaries in [SKILL.md](SKILL.md) before using real repositories. ## Troubleshooting and verification diff --git a/SKILL.md b/SKILL.md index f6a94ba..7164ee1 100644 --- a/SKILL.md +++ b/SKILL.md @@ -55,7 +55,7 @@ Every surface uses one **dark** design on both GitHub themes (decision 21): navy | A set must be reviewed the way GitHub shows it | `contact-sheet` | | Coverage must be reported or a repository verified as done | `lint all`, then `doctor` | -Do not use it for accounts outside the estate. Excluded owners (BunsDev, GoZippy, h-mascot, lmvdz, GalaxyRuler, and per decisions 1 and 2 MetatronAngelAgent and PrompDev) and T3 owners before co-owner sign-off (Pick-Your-AI, Jevelopers-neatHack-2026) are refused with exit 2. +Do not use it for accounts outside the estate. Excluded owners (BunsDev, GoZippy, h-mascot, lmvdz, GalaxyRuler, and per decisions 1 and 2 MetatronAngelAgent and PrompDev) are refused with exit 2, and T3 owners before co-owner sign-off (Pick-Your-AI, Jevelopers-neatHack-2026) are refused with exit 2 unless the global `--allow-t3` is passed for that run. ## Boundaries @@ -73,7 +73,7 @@ Run every command **from inside the target repository's checkout**, with the ski | Step | Command | Gate | |---|---|---| -| 1 | `meta --from-gh OWNER/REPO --out meta.json`, or pass an existing `--meta` file. It also records `pushed_at` and `last_substantive_commit` (CQ-16: the newest default-branch commit whose message does not match `ct-std-v1\|showcase banner and card\|\[skip ci\]`) | Refused owners stop here (exit 2); T3 owners pass only with `--allow-t3`, after an approved `t3-metadata` or `t3-signoff` row. Watching-org forks get metadata only. Archived repositories are in scope (decision 6): the orchestrator unarchives, polishes and re-archives them. | +| 1 | `meta --from-gh OWNER/REPO --out meta.json`, or pass an existing `--meta` file. It also records `pushed_at` and `last_substantive_commit` (CQ-16: the newest default-branch commit whose message does not match `ct-std-v1\|showcase banner and card\|\[skip ci\]`) | Refused owners stop here (exit 2); T3 owners pass only with `--allow-t3`, after an approved `t3-metadata` or `t3-signoff` row. `--allow-t3` also needs, by default, a valid, unexpired record in the private estate file `/decisions/t3-signoffs.yaml` (or the file named by the global `--t3-signoff-file PATH` / `SHOWCASE_T3_SIGNOFF_FILE`, which overrides it), in the format of [references/t3-signoff.md](references/t3-signoff.md) (`id`, `owner`, `repos`, `approved`, `approver`, `approved_on`, `expires`, optional `note`), else exit 2 with `t3-signoff:no-file|missing|invalid|expired|scope|revoked` (`no-file` names the resolved path; the tool never creates the file). Watching-org forks get metadata only. Archived repositories are in scope (decision 6): the orchestrator unarchives, polishes and re-archives them. | | 2 | `describe` | `keep`, `edit` (smallest passing edit, in `proposed`) or `needs-writer`. On `needs-writer`, write the sentence yourself from the template in [references/descriptions.md](references/descriptions.md) and the repository's facts, then re-run `describe --text "..."` until it answers `keep`. The tool never composes it. | | 3 | `topics`, `labels --plan` | Proposals only. Add `--records FILE` to emit change records, `--gh` to print the exact `gh` commands. | | 4 | Apply the approved description and topics on GitHub (the writer), or write them into `meta.json` | Before any art: the banner subtitle, the card and the README tagline come from the description, so rendering first breaks R2 (tagline equals description) the moment the writer applies the new one. | @@ -111,6 +111,8 @@ python3 $S --meta /tmp/meta.json doctor --header Checks (`describe`, `topics`, `labels`, `readme`, `footer`, `meta`, `fingerprint`, `config`) print one JSON object with `command`, `owner`, `repo`, `result` and `logs[]`; `lint` prints one JSON line per file; `org`, `project`, `enterprise` and `content` print one JSON object with `action` (`org doctor --doctor-format tsv|md` prints a row). Generators (`banner`, `card`, `org-banner`, `org-tile`, `intro`, `contact-sheet`) print a short text summary by default; pass the global `--format json` (before the command) for the same JSON object. `doctor` prints a TSV row (`--format json|md` after `doctor`). In text mode a refusal goes to stderr. Log codes are the standard's: `needs-input:`, `art-fail`, `topics-short`, `label-conflict`, `label-referenced`, `label-rewrite-first`, `needs-input:label-references` (a rename held because §6.4 rule 0 needs a checkout: re-plan with `--repo-dir`), `readme-shrink`, `dead-link`, `exempt`, `hold`, `t3-signoff`, `archived`, plus `needs-input:metadata` (a failed metadata capture, exit 4), `needs-input:file-tree` (no checkout and no tree in the metadata, so the type is provisional), `needs-input:visibility`, `needs-input:banner-chips` (fewer than two chips), `needs-input:key-chip` (no gold chip: the type category would repeat the eyebrow), `needs-input:subtitle` (no one-line subtitle: add `subtitle:`), `type-override:`, `type-config:` (from `.github/showcase.yml`) and `bootstrap:edited`. +**Co-owner sign-off (`--allow-t3`).** `--allow-t3` is a global option placed before the command (`python3 $S --allow-t3 --meta meta.json banner`). It applies to every repository command, generator, `lint` and the `org`, `project` and `content` groups, not only `meta`; `meta`, `org` and `project` also accept it after the command name. Pass it only for one explicitly approved invocation, after an approved `t3-metadata` or `t3-signoff` row. Without it a T3 owner (Pick-Your-AI, Jevelopers-neatHack-2026) is refused: exit 2, result `refused`, log `t3-signoff`. Three commands never refuse a T3 owner and need no flag: repository `doctor` always admits T3 owners (the code passes `allow_t3=True`; it still refuses excluded owners, log `exempt`, and HOLD owners, log `hold`) and holds the R7 cell unless `--allow-t3` is passed and a valid sign-off record covers the owner and repository (then R7 is evaluated normally; otherwise it stays `hold`, and `doctor` is read-only so it logs no `t3-signoff` code); `meta` without `--from-gh` only normalizes a local file (only `--from-gh` is gated, and it never checks eligibility); and `fingerprint` never checks the owner. With the flag, excluded owners (log `exempt`) and HOLD owners (log `hold`) are still refused by repository commands and generators; the flag lifts only the T3 refusal. The org actions `about`, `pins`, `health` and `doctor` admit a HOLD org with or without the flag (they pass `allow_hold`), so the HOLD refusal does not apply to them. It holds for that one invocation only and is off again afterwards; the flag itself is never read from a config file or environment (only the record-file path `SHOWCASE_T3_SIGNOFF_FILE` is). Sign-off enforcement is on by default and cannot be turned off: `--allow-t3` is honoured only while a valid, unexpired sign-off record covers the owner and repository (step 1 above), otherwise exit 2 with log `t3-signoff:missing`, `t3-signoff:expired`, `t3-signoff:scope` or `t3-signoff:revoked` (a later or same-day `approved: false` record vetoes the grant for every repository the withdrawal covers, so a co-owner can withdraw one repository from a `*` or multi-repository grant, whether repositories are written as a bare name or `OWNER/NAME` under the owner or any alias; a malformed withdrawal that could cover the request is `t3-signoff:invalid`, never ignored; owner-wide `org profile` and `org pins` repeat the check per repository, so a repository withdrawn from a `*` grant is omitted or held (log `t3-signoff::/`) while the org's other repositories stay authorized; `org profile` additionally requires the sign-off of the org's own `.github` repository, the profile's destination, so a `.github`-scoped withdrawal holds the profile (log `t3-signoff::OWNER/.github`, nothing built or written, existing README and data.json untouched) while the individual repository checks still apply inside it; and `project estate-plan` also holds the Org option update and README when the board's captured Org options name a T3 org without a valid record; `project estate-plan --allow-t3` logs `t3-signoff::` for every T3 org it omits for lack of a valid record; a malformed `repos` is always `t3-signoff:invalid`, even beside a valid unrelated grant; `expires` is optional and absent means no expiry; also `t3-signoff:no-file` when the resolved record file does not exist or no estate root resolves, and `t3-signoff:invalid` when the owner's records break [the schema](references/t3-signoff.schema.json)). There is no opt-out: no flag or environment variable turns the check off. A record dated in the future, or expiring before its approval date, is `t3-signoff:invalid`. Writer jobs carry the granting record as `signoff_id` only (`approval_id` stays untouched: it names the decisions/APPROVED_ACTIONS.yaml row), change records carry `signoff_id`, and repository `doctor` (read-only) adds a trailing `signoff_id` column only when `--allow-t3` is passed, so the documented row shape is unchanged otherwise. Record ids must be unique within the file (a repeated id refuses `t3-signoff:invalid`). Transfer-target holds (an approved pending transfer into a T3 org) apply only when `--allow-t3` is passed. The record file is read, never copied or created; its canonical private path is `/decisions/t3-signoffs.yaml` (estate root: `SHOWCASE_ESTATE_ROOT`, else the skill's parent bundle), resolved in the order `--t3-signoff-file`, `SHOWCASE_T3_SIGNOFF_FILE`, then that default. Sign-off ids already on a job are merged, not replaced, when an Org or association grant is stamped, and direct `project copy-plan --role estate` stamps captured T3 Org options exactly as `project estate-plan` does: the README job of a copied Estate board carries the granting ids of every retained captured T3 Org option directly (not rediscovered from the rendered text), so an option kept under a former login and a different captured label still names its record. A captured Org option is recognized by its description (`T3: `) and by its option name independently: an unresolved or empty description (for example `T3: obsolete-login` or `T3: `) never hides an option name that is a known T3 org's display name, so that option is checked for a valid record and the Org option update and README are held (`t3-signoff::`) when there is none. The Org-options mutation of a retained option carries the option's `signoff_id` (merged with ids already on the job), also when the captured label differs from the org's current names. + ## Orgs, Projects, the Enterprise and Content CLI stdout and stderr use UTF-8, including redirected Windows output, raw footer markup, @@ -166,7 +168,7 @@ python3 $S --meta meta.json content links --base-ref origin/main --diff # in ## Command Reference ```text -python3 /scripts/showcase.py [--config PATH] [--repo-dir DIR | --no-repo-dir] [--meta FILE] [--type TYPE] [--format json|text] [--quiet] [--version] [options] +python3 /scripts/showcase.py [--config PATH] [--repo-dir DIR | --no-repo-dir] [--meta FILE] [--type TYPE] [--format json|text] [--quiet] [--allow-t3] [--t3-signoff-file PATH] [--version] [options] meta --from-gh OWNER/REPO [--out FILE] [--allow-t3] fingerprint [--out-dir docs/images] [--github-output] diff --git a/references/constraints.md b/references/constraints.md index aeb2d6b..d86c5ef 100644 --- a/references/constraints.md +++ b/references/constraints.md @@ -26,7 +26,7 @@ Condensed from `ct-std-v1` §10 and §12. The skill proposes; a single serialize 1. **One serialized writer queue** for every REST call, git push and UI write. Planners and this skill emit change records. 2. **Change record:** `{std, owner, repo, kind: api|file|ui, target, before_sha256, after, rule}`. The writer rejects a record whose `before` no longer matches the live state. Commands that propose writes take `--records FILE`. 3. **Diff first.** GET, compute the delta, write only the delta, GET again to verify. Never write a value that is already equal. -4. **Log every write with its rule** and every skip with a code: `needs-input[:DETAIL]`, `license-review`, `label-conflict`, `label-referenced`, `label-rewrite-first`, `needs-input:label-references`, `hold`, `t3-signoff`, `archived`, `topics-short`, `dead-link`, `fork-parent-missing`, `pages-mismatch`, `pages-readme-index`, `homepage-review`, `readme-shrink`, `apache-notice-review`, `art-fail`, `exempt`, `deploy-relink`, `dns-update`, `ui-danger-dialog`. +4. **Log every write with its rule** and every skip with a code: `needs-input[:DETAIL]`, `license-review`, `label-conflict`, `label-referenced`, `label-rewrite-first`, `needs-input:label-references`, `hold`, `t3-signoff` (always enforced: `t3-signoff:no-file|missing|invalid|expired|scope|revoked`, see [t3-signoff.md](t3-signoff.md)), `archived`, `topics-short`, `dead-link`, `fork-parent-missing`, `pages-mismatch`, `pages-readme-index`, `homepage-review`, `readme-shrink`, `apache-notice-review`, `art-fail`, `exempt`, `deploy-relink`, `dns-update`, `ui-danger-dialog`. 5. **Never commit logs** or the moves list to a public repository: they name private repositories. ## Content commits diff --git a/references/labels.md b/references/labels.md index 3d593e8..fa8276e 100644 --- a/references/labels.md +++ b/references/labels.md @@ -15,7 +15,7 @@ GitHub limits: hex colours without `#`; descriptions at most 100 characters; ren - **`.github` repos** always get the standard set, whatever their `has_issues`. - **AI-Ascension:** keeps its `labels.yml` and color tokens. R6 there means only that `dependencies` exists (`0366d6`, the standard set description); its Dependabot configs use `labels: ["dependencies"]`. - **Don't apply** to T2-R, T3, HOLD, forks or archived repos. -- `labels --plan` (one repository) applies the same gates as `org labels`: a family on hold is refused (exit 2) unless `--allow-hold`; a T3 owner is refused (exit 2, `t3-signoff`) until co-owner sign-off, as `org labels` refuses it without `--allow-t3`; AI-Ascension gets only the `dependencies` create or patch; a repository with `has_issues: false` other than `.github`, forks, archived repositories (`archived`), T2-R and romgenie repositories answer `n/a`. Metadata without labels (a missing key, or `[]`, which is also what a failed capture records) is never planned against: whenever argv, writer jobs (`--jobs`), records (`--records`) or `gh` lines leave, `labels` reads the live labels first (read-only GET) and exits 4 (`needs-input:gh`) when it cannot. Writer job ids come from the operation (`p3.2-OWNER__REPO-label--[-to-]-` for `labels --plan` and `org labels`, `p3:label:OWNER__REPO::[-to-]:` for `adopt`), so a re-plan never reuses an id for other content. +- `labels --plan` (one repository) applies the same gates as `org labels`: a family on hold is refused (exit 2) unless `--allow-hold`; a T3 owner is refused (exit 2, `t3-signoff`) until co-owner sign-off, as `org labels` refuses it without `--allow-t3` (and, with it, without a valid record in the estate sign-off file, `t3-signoff:`; [t3-signoff.md](t3-signoff.md)); AI-Ascension gets only the `dependencies` create or patch; a repository with `has_issues: false` other than `.github`, forks, archived repositories (`archived`), T2-R and romgenie repositories answer `n/a`. Metadata without labels (a missing key, or `[]`, which is also what a failed capture records) is never planned against: whenever argv, writer jobs (`--jobs`), records (`--records`) or `gh` lines leave, `labels` reads the live labels first (read-only GET) and exits 4 (`needs-input:gh`) when it cannot. Writer job ids come from the operation (`p3.2-OWNER__REPO-label--[-to-]-` for `labels --plan` and `org labels`, `p3:label:OWNER__REPO::[-to-]:` for `adopt`), so a re-plan never reuses an id for other content. - **Org default labels** [UI]: all T1 orgs and the T2 product orgs except AI-Ascension, which uses its own `labels.yml`. None for the romgenie orgs or CompleteDotTech. Removing GitHub's default entries from org defaults is exempt from the approval list. ## The standard set: 23 labels diff --git a/references/org.md b/references/org.md index 99f0a98..ac13dce 100644 --- a/references/org.md +++ b/references/org.md @@ -14,7 +14,7 @@ python3 $S org health ORG [--dot-github CHECKOUT] # O4: §8.1 python3 $S org doctor ORG --snapshot org-snapshot.json [--dot-github CHECKOUT] [--avatar avatar.png] [--doctor-format tsv|json|md] [--header] ``` -Every action takes `ORG` (any former login resolves, see below), `--registry`, `--meta-dir`, `--decisions`, `--approved`, `--approved-yaml`, `--actions`, `--allow-t3` and `--jobs FILE` (append the writer jobs as JSON lines). +Every action takes `ORG` (any former login resolves, see below), `--registry`, `--meta-dir`, `--decisions`, `--approved`, `--approved-yaml`, `--actions`, `--allow-t3` and `--jobs FILE` (append the writer jobs as JSON lines). The global `--allow-t3` (before the command) is equivalent. Either needs a valid sign-off record by default (`/decisions/t3-signoffs.yaml` or `--t3-signoff-file`; `t3-signoff:no-file|missing|invalid|expired|scope|revoked` otherwise, see [t3-signoff.md](t3-signoff.md)); there is no opt-out. | Action | Reads | Emits | Writes | |---|---|---|---| diff --git a/references/projects.md b/references/projects.md index 32dc731..c292e56 100644 --- a/references/projects.md +++ b/references/projects.md @@ -36,7 +36,7 @@ python3 $S project estate-plan [--copied after-copy.json] [--user-id ID] [--jobs | `readme` | Fills the §13.7 README template for a copied board (view numbers from the post-copy capture). The same privacy gate as `copy-plan`: a board that is public (as captured) or may go public (an org board with an approved visibility item) gets no repository in its Links line, from `--repos` or the capture (`readme-links:repositories-omitted (the board may go public)`). The Fields table and the Workflow line document the board's own options (a kept scale such as P0/P1/P2, its workstreams, its Status flow), and in `copy-plan` the options the plan's jobs leave; emoji in option text are dropped from the README (`readme-emoji-stripped:/