Repository navigation
feat: Multi-Forge implementation (features, claims, Release V3, GitLab provider) -> dev - #172
Merged
Merged
Conversation
) ADR-0017 makes profiles (governance) and features (optional project-scope capabilities) orthogonal. This change adds the model and the read side; switching comes next in the lane. - data/features.json: forge-github / forge-gitlab — project scope, symmetric conflicts, work-forge flag, declared legacy_default (no hard-coded default). - manifest: Feature, Distribution.features / legacy_default_feature, Distribution.feature(); the catalogue refuses a machine-scope entry, an asymmetric conflict, a component claimed by two features, an undeclared component reference and an undeclared legacy default. - state: schema V2 with active_features. V1 still loads (no field); a V2 state naming a feature this release does not know refuses as INSTALL_STATE_CORRUPTED (upgrade the CLI, never downgrade the state), and the existing schema guard still refuses a newer schema. - features.project_install_state(): the single projection every read path will share; V1 projects to the declared legacy default; a V2 state activating two work forges refuses as STATE_CONFLICTING_WORK_FORGE_FEATURES. The projection never writes. - features.migrate_state(): stamps the current schema and seeds the legacy default exactly once, inside the existing install transaction (state-last); it plans no file change of its own, so a managed file that is already absent stays absent through the migration. - installer: a fresh state seeds the default feature (a fresh Standard install keeps shipping the GitHub templates, as it always has); a no-op install still saves the state when it was V1, which is what makes the migration persist. - gitlab-templates component and templates/gitlab/ (owned by forge-gitlab; the .gitlab/ equivalents of the GitHub templates). - New refusal codes: STATE_CONFLICTING_WORK_FORGE_FEATURES, FEATURE_UNKNOWN. - CI: tests.test_lifecycle_features runs in the lifecycle job. Verified: 295 lifecycle tests OK (21 new), machine/CLI suites OK, gates (conventions, scope, complexity, LOC) green. Refs #162
`ainative feature enable | disable | switch | status` moves a project between optional capabilities with the profile path's guarantees unchanged: project lifecycle lock, reload, project the effective V2 set, plan, apply, verify, state last, commit. A V1 state migrates inside the same transaction. - profiles.json: `github-templates` leaves the standard profile; the feature `forge-github` owns it now (a component belongs to exactly one owner), so `feature switch none` really is Generic Git. - planner: the wanted set is profile components plus the active features' components, computed by one shared function the installer also records from (plan and record cannot drift). Feature files are seeded when a feature is enabled; ordinary maintenance never re-seeds an absent feature file — a file the user removed stays removed until an explicit transition (ADR-0017 §5). - installer.set_features(): the transition semantics. `enable` refuses a conflicting active feature with the remedy (`FEATURE_CONFLICT`, never an implicit disable); `switch` replaces the work forge in one plan; `switch none` removes every work forge and nothing else. A no-op transition changes nothing — including the state bytes — unless it still has something to record (migration, moved set, dropped component records). - feature_cli.py: the command surface, split out of cli.py so the dispatcher stays under its LOC budget; status derives from the shared projection. - New refusal code FEATURE_CONFLICT. - Tests: switching, disable preserves a modified file, absent feature file stays absent through installs and is re-seeded by an explicit transition, the migration fixture (a V1 state whose GitHub template is absent stays absent), a round trip without user-data loss, CLI status/switch. - Docs: DISTRIBUTION-LIFECYCLE §18 and CHANGELOG. Verified: 307 lifecycle tests OK (33 in the features suite), CLI/machine suites OK, gates green (conventions, scope, complexity, LOC — cli.py back under 800), five non-vacuity guards re-proved. Refs #162
…journal (#163) ADR-0018's local half: the harness observes remote facts, this layer records them deterministically and never decides by guessing. - ainative/forge.py: `resolve_observed_work_authority()` — pure (no network, no credentials, no writes, no push authorization). Priority: explicit reference, then harness declaration, then exactly one compatible observed candidate; otherwise WORK_AUTHORITY_UNAVAILABLE / _AMBIGUOUS / _MISMATCH. Only github.com and gitlab.com are read as providers; a self-hosted host is `unknown`, never inferred from its name; a fork is ambiguity, never "prefer origin". - ainative/claims.py: canonical identities (`<provider>:principal:<id>`, `<provider>:<kind>:<id>`), UTC-normalized timestamps (a naive one refuses), total winner ordering with CLAIM_CONFLICT for unorderable duplicates, and the ClaimAttempt journal under `.ai-native/state/claim-attempts/` — PENDING written durably before the remote POST, outcomes CONFIRMED/LOST/CONFLICT/UNCERTAIN/ABANDONED, an unwritable or corrupt journal fails closed, abandon is an explicit local operator transition that keeps the record, and an abandoned record is final. - ainative/claim_cli.py + `ainative claim-attempt list|inspect|abandon --confirm`: the recovery surface. No retry path exists by design. - New refusal codes: WORK_AUTHORITY_*, CLAIM_INVALID, CLAIM_CONFLICT, CLAIM_UNCERTAIN, CLAIM_JOURNAL_UNAVAILABLE. - Tests: 27 in tests/test_forge_claims.py, including the invariant that a pending attempt survives a lifecycle update byte-identically and the CLI confirmation flow. CI runs them in the lifecycle job. Verified: 308 lifecycle tests OK, knowledge/machine/CLI suites OK, gates green (conventions, scope, complexity, LOC). Refs #163
The engineering-method component installs templates/AGENTS.md (Work Authority vocabulary, ADR-0018) instead of the repository's own AGENTS.md, which stays GitHub-specific. Identical to the root file except the work-management section; the fixture distribution ships both files.
…sion (#164) `ainative forge detect|status` observe the project's Git remotes and render the Work Authority resolution. Read-only by construction: the only subprocess is `git remote`, the only inputs are the pure resolver, the install state and the claim journal — zero network, zero credentials, zero writes, zero persistent trust. A fork renders as AMBIGUOUS (both candidates shown, no preference for origin) and an unknown host as UNAVAILABLE, both exit 0: detection is a diagnostic, the refusal belongs to the mutation path. - ainative/observation.py: remote reading, `forge_picture()` (resolution as state), `project_view()` (features + forge + unresolved claim attempts) and the doctor text lines. An unreadable claim journal is displayed, not a crash. - ainative/forge_cli.py: the command surface; `status` adds the effective feature set (shared projection) and the unresolved claim count. - Doctor extension: profile/features/forge/claim summary in both outputs, never a credential (`doctor --json` gains features, forge, claim_attempts). - status.py: reports the effective feature set through the same projection (`features`, `features_projected_from_legacy`), closing the read-only parity requirement for `status`. - ainative/cli_support.py: the small shared plumbing (emit/project/report/ plan text) extracted so cli.py stays the dispatcher under its LOC budget and the command modules stop importing render callables through cli.py. - Tests: 7 new observation tests (resolution, fork rendering, unknown host, features+claims in status, byte-identical project across detect/status/ doctor) and a status/projection parity test for a V1 state. Verified: 309 lifecycle tests OK, 139 CLI/knowledge/machine suites OK, gates green (conventions, scope, complexity, LOC: cli.py 757). Refs #164
#165) PR-4A. `resolve_release_source()` (ainative/lifecycle/release_source.py) is now the only function that decides where a release comes from, and `update`, `update check`, `status` and `doctor` all consume it — so the answer cannot differ between commands (ADR-0019 sections 1-3). - Selector rules exactly as frozen: local pair -> mirror; LOCAL_DIR without provider=local -> UPDATE_SOURCE_CONFLICT; URL mixed with any selector -> UPDATE_SOURCE_CONFLICT; URL alone -> anonymous; named provider -> machine config or built-in; nothing -> machine default_provider, else GitHub.com. Validation before precedence: nothing is ordered silently, and the old "unknown provider" refusal is preserved. - Machine scope `~/.ai-native/release-providers.json` (schema 1): reserved names (github/gitlab/local) cannot be redefined, a named provider is anonymous in V1 (a custom auth_origin is refused), a future schema refuses rather than guessing. `default_provider: local` requires a configured directory. New refusal codes: UPDATE_SOURCE_CONFLICT, RELEASE_CONFIG_INVALID. - `provider.build()` delegates to the resolver; the selector constants are re-exported so existing imports keep working. - Observability: `status` and `doctor` display the effective source, the selection reason, authenticated yes/no and the auth origin — never a secret (`describe()` turns a selector conflict into a displayed state, so the diagnostics keep diagnosing while the updater refuses). - Tests: 19 in tests/test_release_source.py (selector matrix, machine config, provider construction, status/doctor parity); CI runs them in the lifecycle job. Verified: 309 lifecycle tests OK, 123 update/CLI suites OK, gates green (conventions, scope, complexity 0 findings after splitting the resolver, LOC 0 warnings). Refs #165
…on chain (#165) PR-4B + PR-4C. ainative/lifecycle/release_v3.py owns the V3 vocabulary and the fail-closed policies around it (ADR-0019 sections 7-10): - Candidate policy: installable versions are SemVer without build metadata (1.2.3, 1.2.3-rc.1); `1.2.3+build1` refuses as RELEASE_BUILD_METADATA_UNSUPPORTED; ordering uses version precedence, never string order; two identities on one version refuse as RELEASE_DUPLICATE_VERSION (never "first match"); an enumeration that stopped at its bounds refuses as RELEASE_ENUMERATION_INCOMPLETE; a complete channel with nothing refuses as RELEASE_NO_CANDIDATE. - The external anchor travels WITH the candidate (provider metadata): manifest sha256 + size are verified BEFORE the document is parsed — RELEASE_INTEGRITY_METADATA_MISSING / _INVALID for absent/malformed anchors, UPDATE_INTEGRITY_FAILED for any mismatch. A hostile manifest can never influence whether its own bytes are trusted. - ReleaseManifest V3: schema, protocol, version, channel, compatibility, artifacts, provenance — strictly validated (plain filenames, exact one lifecycle artifact, canonical versions). A newer protocol refuses as CLI_UPDATE_REQUIRED, mirroring the bridge; anything else is an integrity refusal. - The exact version chain: candidate == manifest == compatibility.runtime_version == artifact == artifact filename == lifecycle-protocol.json release_version, with the broken link named in the refusal detail. `resolve_manifest()` formalizes the order: enumerate -> select -> verify -> parse -> chain. Tests: 32 in tests/test_release_v3.py (SemVer policy, selection, anchors, manifest refusals, every broken chain link, resolution order with a fake provider). CI runs them in the lifecycle job. Verified: 309 lifecycle tests OK, complexity 0 findings, LOC 0 warnings, scope within tolerance. Refs #165
#165) PR-4D + PR-4E. Each provider implements the `release_v3` contract and nothing else: enumerate, fetch the manifest, fetch an artifact. Every trust decision stays in release_v3 — the provider that fetched a manifest never decides whether to believe it (ADR-0019 sections 9-11). - GitHubReleaseProvider: bounded enumeration of the releases API (a full page says `complete=False`, so selection refuses instead of picking the best of what it saw); drafts, non-SemVer tags and releases without an `ainative-release-v3.json` asset are not candidates (a V2-era release is not a broken V3 one); the manifest asset's digest/size travel as the candidate's anchor; artifacts and manifests are fetched through the PR-0A transport (asset API locator preferred, `browser_download_url` fallback, octet-stream, bearer only at `api.github.com`). - AnonymousReleaseApiProvider: `AINATIVE_UPDATE_URL` as one GitHub-shaped release document, never authenticated; a document without a V3 manifest yields no candidate (`RELEASE_NO_CANDIDATE`), never a silent V2 fallback. - LocalReleaseProvider: `releases.json` channels carry the manifest locator with its size and SHA-256; the mirror executes the same logical chain as a network source, traversal-checked, bounded, and never trusted for being on disk. A V2-era channel entry is not a candidate. - `ReleaseCandidate` gains `manifest_locator` (provider-internal, opaque to the contract); `provider.environment_token()` exposes the environment token publicly for the V3 providers under the same origin rule. Tests: 16 in tests/test_release_providers.py, including an end-to-end `resolve_manifest` against the local mirror and against a scripted GitHub server, a tampered-manifest refusal, a bounded-listing refusal and a path-traversal refusal. CI runs them in the lifecycle job. Verified: 309 lifecycle tests OK, 67 release tests OK, complexity 0 findings, LOC 0 warnings, scope within tolerance. Refs #165
…#165) PR-4 completion. `update check` and `update` now resolve through `resolve_v3_candidate()`: the resolved source is enumerated (bounded), the newest V3 candidate of the channel is selected, and the manifest is fetched and anchor-verified only when an update is applied — a check needs the version, nothing more. - A source that hits its enumeration bounds refuses (RELEASE_ENUMERATION_INCOMPLETE): partial results are never a reason to change generation. - A complete channel with no V3 candidate is a V2-era mirror, and the caller speaks the existing V2 path to it (deterministic, disclosed fallback — the bridge-era releases stay consumable). Everything else propagates. - apply(): V3 fetch verifies the artifact against the manifest's own size+SHA-256 before extraction, then the existing transaction applies the payload; the V3 bundle declares lifecycle protocol 3 (`_distribution_root` gained an `expected_protocol`), and a bundle carrying the V2 protocol refuses as UPDATE_VERSION_MISMATCH. - `provider.build_v3()` is the second patchable seam; the four tests that stub the V2 provider now stub it explicitly. Tests: 5 new end-to-end tests on a local V3 mirror (check, apply + rollback, tampered manifest refused with zero writes, protocol mismatch refused, CLI report). 314 lifecycle tests OK; release/CLI suites OK; gates green. Refs #165
#166) PR-5 core. The GitLab provider implements the V3 contract (ADR-0019 §11): - Releases API for discovery only; the Generic Package Registry is the canonical distribution and integrity surface. Release Link URLs are never integrity roots — the anchor is the package file API's `file_sha256` and `size` (absent -> RELEASE_INTEGRITY_METADATA_MISSING, malformed -> RELEASE_INTEGRITY_METADATA_INVALID), and the manifest file lookup requires exactly one `ainative-release-v3.json` (0 -> RELEASE_MANIFEST_MISSING, >1 -> RELEASE_MANIFEST_AMBIGUOUS). - One project identity (`release_project_ref`) is used for Releases, packages and package files — no second project configuration. `release_project_ref` is a numeric id or namespace path, never a URL; it is machine-scope config (`providers.gitlab`), while `github`/`local` remain non-redefinable. - Exact package match (`type == generic`, canonical package name, selected version) with exactly one record, and bounded pagination (a full page refuses as incomplete, never a partial best-of). - Authentication shapes differ per provider: `ReleaseProviderEndpointConfig` gained `auth_header`/`auth_prefix`. GitLab sends `PRIVATE-TOKEN: <token>` from `GITLAB_TOKEN`, only at its configured origin; GitHub keeps `Authorization: Bearer`, unchanged. - GitLab is V3-only: `supports_v2_fallback = False`, and a complete channel without a V3 candidate refuses as RELEASE_NO_CANDIDATE instead of falling back to a generation that does not exist there. Tests: 10 GitLab provider tests on a scripted GitLab (exact-identity match, private-token confinement, duplicate packages, every manifest/integrity refusal, pagination bounds, full chain, anonymous endpoints) plus 3 config tests; and the three purity suites required by §74 (tests/purity/test_dependency_purity.py, test_contract_purity.py, test_distributed_policy_purity.py) — neutral modules do not import provider implementations, the neutral helpers behave identically for both forges, and the distributed policy carries no GitHub-only authority. Verified: 314 lifecycle tests OK, release suites OK, purity 7/7, complexity 0 findings, LOC 0 warnings. Refs #166
This was referenced Sep 16, 2026
Rwanbt
added a commit
that referenced
this pull request
Sep 17, 2026
* fix(lifecycle): fail-closed release version chain and runtime freshness A release could publish a v2.2.2 tag with a 2.2.1 bundle inside (AUD-201), and an old lifecycle runtime could silently apply a new stack it does not know (AUD-202). - provider: accept only ainative-dev-stack-<release.version>.zip, official and mirror alike; a mismatch is UPDATE_VERSION_MISMATCH before any download - updater: refuse a bundle whose internal VERSION differs from the release; require runtime == target with CLI_UPDATE_REQUIRED before download and before any write; refusals now write nothing, cache included - status/doctor: surface runtime_ready so a user sees that the CLI must be upgraded first - #131: every cache read reconciles a stale UPDATE_AVAILABLE notice against the live project version (UP_TO_DATE, fail-safe on malformed values), reads never rewrite the cache; rollback dry-run says would roll back and writes nothing - tests: version-chain mutation cases A-G, runtime freshness, stale cache; new non-vacuity cases prove each guard blocks (39/39) * ci(release): version gate, real upgrade E2E, SHA-pinned actions, supply-chain baseline - release workflow refuses tag != v\2.2.2 and re-verifies every artifact (bundle filename+internal VERSION, wheel metadata, sdist PKG-INFO) through scripts/check_release_versions.py - new upgrade-e2e job (Linux/Windows/macOS): scripts/lifecycle_upgrade_e2e.py builds two real wheels, installs N in a fresh venv, proves CLI_UPDATE_REQUIRED with zero writes, upgrades the runtime, applies N+1, rolls back, and refuses a tampered bundle - all third-party Actions pinned to full commit SHAs; Dependabot for github-actions; SECURITY.md reporting path; CODEOWNERS for sensitive zones (#129) - Python 3.8 matrix entry best-effort is now actually wired; orphan tests/mv00 Multi-Vault feasibility suites run in CI * fix(anti-debt): fingerprint secrets instead of previewing them; fail loudly on parser defects - scan_security: trufflehog/gitleaks findings carry sha256(secret)[:12], never raw[:8], in ids or evidence (AUD-203); stable for the same input, distinct across secrets - scan_security/scan_deps/scan_code: a defect in a scanner's own parser is no longer converted into a warning entry (the clippy line_start class of #127); missing binaries, timeouts, non-zero exits and invalid external JSON still degrade to structured warnings - AI_CONTEXT: execution model documented as shell=False with the controlled cmd /c shim exception - tests: no-secret-fragment, stable/distinct fingerprint, parser-defect-propagates; CI step added * docs(release): v2.2.2 - README EN/FR parity, lifecycle docs, CHANGELOG, version bump - README EN and FR now describe the same operational facts: CLI-first upgrade then per-project update, the fail-closed release chain, Verified Work Plane, K5 STOP, Multi-Vault GUARDED; FR regains the missing components 13-17 and the Verified section - UPDATING.md and docs/DISTRIBUTION-LIFECYCLE.md document CLI_UPDATE_REQUIRED, UPDATE_VERSION_MISMATCH and the zero-write refusal order - K0-A capability probe flagged historical and pointed at the convergence matrix; K0-B index annotated - CHANGELOG 2.2.2; VERSION/__init__/AGENTS.md/README pins bumped together; scope figures re-measured * feat(lifecycle): first-run product readiness - hook wiring, doctor, git policy, path gate - init now merges one owned PostToolUse entry into .claude/settings.json (json_hook component): user keys and groups preserved, idempotent, uninstall/rollback remove only the owned entry, unparsable JSON refused without rewriting - doctor gains an environment section (python, git repository/HEAD, node, hook status, harness integration, vault, Obsidian API, graphify, machine manifest, trust anchor) with OK/ABSENT_OPTIONAL/DEGRADED/FAIL; missing automation fails doctor, optional tools do not - Verified refuses a project that is not a Git repository (GIT_REPOSITORY_REQUIRED); Standard installs with an explicit degradation notice - action vocabulary no longer lies: BLOCK_WRITE/BLOCK_REMOVE replaced by REGION_WRITE/REGION_REMOVE/HOOK_WRITE/HOOK_REMOVE in plans (legacy journal names still readable) - Knowledge fresh-init fixed (#138): the managed .gitignore region carries .ai-native/state/, refusals print actionable remedies; knowledge state and trust anchor are owner-only on POSIX - machine paths removed from distributed files (Mavis-generated opencode artifacts deleted, enforcement script defaults removed, personal email out of fixtures); scripts/check_personal_paths.py gates tracked files with a reasoned allowlist and a mutation test * ci: run the hook, first-run policy and machine-path gates * feat(workplane): trust init onboarding, schema validation, stable refusals - trust_schema owns the approval-root/policy shapes: validation through contracts.validate_artifact (one structural owner) plus semantic coherence (self-commitments, predicate vocabulary, observable facts); codes TRUST_ROOT_INVALID, TRUST_POLICY_INVALID, TRUST_SCHEMA_UNSUPPORTED - 'ainative trust init' scaffolds a minimal coherent pair and claims no authority; bootstrap remains the ceremony; predicate mismatch between policy and bootstrap is refused - bootstrap validates before reading: {} no longer raises KeyError with exit 1 (NOT_CONVERGED's code); workplane refusals now print 'refused: CODE: message' on stderr and the same record as JSON on stdout, always exit 2 - scripts/verified_first_run_e2e.py: fresh wheel + venv + git project, full governed workflow to CONVERGED through the CLI only, plus the refusal battery; CI job on 3 OS; tests.test_trust_onboarding covers scaffold and refusals * feat(lifecycle): versioned update protocol v2, authenticated checks, atomic release - protocol v2: bundles are ainative-lifecycle-v2-<version>.zip with a lifecycle-protocol.json at the root and the payload under stack/; runtimes <= v2.2.2 cannot resolve it even from a mirror (layout containment, AUD-205), and v2.2.2 refuses at the runtime gate first - updater validates the protocol document (version, release, payload root) before trusting the payload - GITHUB_TOKEN/GH_TOKEN support for update checks: sent as a bearer header, never logged, never cached; 403/429 reported as a rate-limit diagnosis - 'update check --strict' exits non-zero when the source could not be consulted; default documents exit 0 != reachable - release workflow: draft -> upload (no clobber) -> verify names/sizes/digests -> publish, with GitHub build-provenance attestations; scripts/check_published_assets.py and the extended check_release_versions.py (--without-bundle for the PyPI half) - new publish-pypi.yml using PyPI Trusted Publishing (OIDC); docs/RELEASING.md documents setup, verification and the no-clobber recovery path; tests for the transport, containment and both gates * feat(machine): machine ownership manifest, safe uninstall, dogfood fix (#137, #18-#20) - every global install now records ~/.ai-native/machine.json: each link, managed block and rendered file with its source or digest and the stack version - scripts/machine_lifecycle.py owns the reversal rule: recorded+unmodified assets are removed (links/junctions are removed, never followed), user files and user-modified assets are preserved and reported, empty directories the uninstall created are pruned up to the standard harness roots, the manifest is deleted only after success - install_agents.py --uninstall [--dry-run] prints the plan; dry runs are pure (tests caught the helpers mutating during classification); rendered files are written as bytes so their recorded digest matches the file on Windows - lifecycle_dogfood.py force-adds .ai-native in its scratch clone (#137: the repo's own gitignore kept the anchor uncommitted, so verified_anchor counted zero commits); the first full dogfood run then caught a stale non-vacuity anchor, fixed - scripts/tests/test_machine_lifecycle.py covers record, dry-run purity, user-file preservation, modified-asset preservation, idempotence and malformed manifests * docs(release): v2.3.0 - support docs, machine lifecycle docs, executable first runs - SUPPORT.md (support matrix, reporting path, known limitations) and CODE_OF_CONDUCT.md - README EN/FR: Verified first run is trust init + bootstrap; the PostToolUse hook is configured by init, not by hand; machine integration records a manifest and has a real uninstall; non-git behaviour documented - UPDATING.md and docs/DISTRIBUTION-LIFECYCLE.md: machine update/uninstall, the claude-hook component, protocol v2 containment - version bump 2.3.0 across VERSION, package, AGENTS.md header and README pins; CHANGELOG; scope figures re-measured - both E2E scripts now build from a hermetic copy of the tree: a developer's stale build/ cache made pip wheel fail with WinError 183 and could contaminate the artifact under test * fix(lifecycle): managed files get umask modes; clean-install works in a Git project - write_atomic/write_bytes_atomic apply the umask-compatible mode after replace: NamedTemporaryFile's 0600 leaked into every managed project file on POSIX (private state still calls write_atomic_private explicitly) - lifecycle_clean_install.py initialises the fresh project as a Git repository (Verified refuses without one) and gains a non-Git leg proving the documented policy: Standard installs with a notice, Verified refuses with zero writes - the non-Git leg isolates Git discovery with GIT_CEILING_DIRECTORIES so a developer's enclosing repository cannot change the result * fix(clean-install): the payload/checkout comparison ignores the project's own .git * fix(hooks): the PostToolUse wrapper reads stdin through the pipeline first [Console]::In.ReadToEnd() returned empty on the Windows CI runners while \ held the payload (and the reverse was observed locally); whichever has the JSON wins, and a skip is reported on stderr instead of staying silent. The same fix is applied to the shared global hook wrapper. * fix(hooks): the wrapper reports why it skipped instead of exiting silently * fix(hooks): hand the payload to Python by file, with visible skips PowerShell's pipe to a native executable is not byte-stable across host shapes (the console-less runners delivered an empty or mangled stream). The wrapper now tries pipeline, raw stdin handle and Console.In in turn, writes the exact bytes to a temp file, and calls update_on_edit.py --payload-file; the script reports an empty or non-JSON payload on stderr instead of skipping silently. Get-Command matches are enumerated (-All) so a WindowsApps Store alias can never win over a real interpreter, the global hook keeps its own stack-root resolution, and the test makes the running interpreter visible on PATH. * fix(release): the published-asset report never lands inside dist The v2.3.0 release workflow aborted after every asset had uploaded and attested: the verification step wrote its assets.json into dist/, and the checker counted it as an unbuilt extra asset. The report now lives in RUNNER_TEMP, the checker excludes its own report file defensively, and the regression is pinned by test. * release: 2.4.2 * release: 2.4.3 Ships the #153 OpenCode plugin runtime fix and closes the published-artifact gap it exposed: the staged payload carried no adapters/ tree, so a wheel install rendered no plugin (visible SKIP) and the documented machine integration existed for checkout users only. - adapters/ (OpenCode + Claude Code) joins the wheel and sdist payload, so `ainative machine init` renders the plugin from the installed distribution. - A durable runtime gate (the opencode-plugin CI job, inside the Production Gate) loads the rendered plugin in a real runtime with no global Bun, drives edit/write, refuses a file over the blocking limit, reads that limit from conventions.json, and proves AI_SUMMARY.md is regenerated. - The same gate runs against the plugin rendered by a wheel installed in a fresh venv, so the published artifact is what is tested. - Version labels move together (VERSION, ainative.__version__, AGENTS.md stack-version, scope table); the READMEs, UPDATING, RELEASING, PUBLIC-PILOT and the Multi-Vault operator guide pin v2.4.3. * fix(opencode-e2e): compare resolved manifest paths, not Windows short names * fix(lifecycle): confine release credentials to the configured origin (#158) (#167) The release transport attached GITHUB_TOKEN/GH_TOKEN to every request it made — including an artifact URL named by the metadata of a custom AINATIVE_UPDATE_URL — and urllib's redirect handler copied Authorization to any redirect target (both reproduced before changing production code). A new transport module follows redirects itself, one policy-checked hop at a time: release metadata may not leave its own origin, an artifact redirect is followed anonymously with every credential recomputed per hop, https -> http downgrades and URLs carrying userinfo are refused, HTTPS is required throughout, and redirect chains are bounded at five. GitHub private release assets are fetched through the release asset API URL (Accept: application/octet-stream); the 302 to the CDN is stripped of credentials. The credential origin is endpoint configuration (auth_origin), never derived from the URL being fetched. AINATIVE_UPDATE_URL is anonymous by construction: it can no longer inherit the provider token. Documented as a breaking behavior change in the CHANGELOG and the distribution lifecycle documentation. Refs #158 * docs(adr): ADR-MF-02 - neutral workflow, work authority and claims (Refs #159) (#169) * docs(adr): ADR-MF-03 - release source resolution, manifest V3, providers (Refs #161) (#170) * docs(adr): ADR-MF-01 - Generic Git, features and state V2 (Refs #160) (#168) * feat(lifecycle): report a future lifecycle protocol as CLI_UPDATE_REQUIRED (#157) (#171) A V2 runtime meeting a V3 publication used to say UPDATE_INTEGRITY_METADATA_MISSING - "no lifecycle bundle to verify" - which sends every user looking for a publishing defect instead of upgrading the CLI. The two situations are now distinct: - a release that publishes a lifecycle bundle or manifest for a protocol newer than this runtime's (ainative-lifecycle-v3-*, ainative-release-v3.json) resolves as a refusal: CLI_UPDATE_REQUIRED with the upgrade command, and `update check` reports it as available with a CLI upgrade required; - a release that publishes no lifecycle bundle at all keeps UPDATE_INTEGRITY_METADATA_MISSING, so a broken publication is never mistaken for future protocol. `upgrade_command` moves to provider.py (a release-source refusal must name the upgrade path, and provider must not import the updater); updater re-exports it. Adds the bridge publication gate: scripts/check_bridge_release.py resolves a declared V3_BRIDGE_RELEASE through the same V2 selection rules a user's updater uses and blocks a V3 publication when the bridge is missing, unpublished, or carries no lifecycle bundle. The release workflow runs it when the repository variable V3_BRIDGE_RELEASE is set (explicit SKIP otherwise). The manual stale-runtime escape path is documented in RELEASING.md and the distribution lifecycle documentation. Characterization: the new tests failed on the pre-change tree with UPDATE_INTEGRITY_METADATA_MISSING / CHECK_FAILED; they pass after the change. Refs #157 * feat: Multi-Forge implementation (features, claims, Release V3, GitLab provider) -> dev (#172) * feat(lifecycle): feature model, State V2 and the shared projection (#162) ADR-0017 makes profiles (governance) and features (optional project-scope capabilities) orthogonal. This change adds the model and the read side; switching comes next in the lane. - data/features.json: forge-github / forge-gitlab — project scope, symmetric conflicts, work-forge flag, declared legacy_default (no hard-coded default). - manifest: Feature, Distribution.features / legacy_default_feature, Distribution.feature(); the catalogue refuses a machine-scope entry, an asymmetric conflict, a component claimed by two features, an undeclared component reference and an undeclared legacy default. - state: schema V2 with active_features. V1 still loads (no field); a V2 state naming a feature this release does not know refuses as INSTALL_STATE_CORRUPTED (upgrade the CLI, never downgrade the state), and the existing schema guard still refuses a newer schema. - features.project_install_state(): the single projection every read path will share; V1 projects to the declared legacy default; a V2 state activating two work forges refuses as STATE_CONFLICTING_WORK_FORGE_FEATURES. The projection never writes. - features.migrate_state(): stamps the current schema and seeds the legacy default exactly once, inside the existing install transaction (state-last); it plans no file change of its own, so a managed file that is already absent stays absent through the migration. - installer: a fresh state seeds the default feature (a fresh Standard install keeps shipping the GitHub templates, as it always has); a no-op install still saves the state when it was V1, which is what makes the migration persist. - gitlab-templates component and templates/gitlab/ (owned by forge-gitlab; the .gitlab/ equivalents of the GitHub templates). - New refusal codes: STATE_CONFLICTING_WORK_FORGE_FEATURES, FEATURE_UNKNOWN. - CI: tests.test_lifecycle_features runs in the lifecycle job. Verified: 295 lifecycle tests OK (21 new), machine/CLI suites OK, gates (conventions, scope, complexity, LOC) green. Refs #162 * feat(lifecycle): feature switching in one transaction (#162) `ainative feature enable | disable | switch | status` moves a project between optional capabilities with the profile path's guarantees unchanged: project lifecycle lock, reload, project the effective V2 set, plan, apply, verify, state last, commit. A V1 state migrates inside the same transaction. - profiles.json: `github-templates` leaves the standard profile; the feature `forge-github` owns it now (a component belongs to exactly one owner), so `feature switch none` really is Generic Git. - planner: the wanted set is profile components plus the active features' components, computed by one shared function the installer also records from (plan and record cannot drift). Feature files are seeded when a feature is enabled; ordinary maintenance never re-seeds an absent feature file — a file the user removed stays removed until an explicit transition (ADR-0017 §5). - installer.set_features(): the transition semantics. `enable` refuses a conflicting active feature with the remedy (`FEATURE_CONFLICT`, never an implicit disable); `switch` replaces the work forge in one plan; `switch none` removes every work forge and nothing else. A no-op transition changes nothing — including the state bytes — unless it still has something to record (migration, moved set, dropped component records). - feature_cli.py: the command surface, split out of cli.py so the dispatcher stays under its LOC budget; status derives from the shared projection. - New refusal code FEATURE_CONFLICT. - Tests: switching, disable preserves a modified file, absent feature file stays absent through installs and is re-seeded by an explicit transition, the migration fixture (a V1 state whose GitHub template is absent stays absent), a round trip without user-data loss, CLI status/switch. - Docs: DISTRIBUTION-LIFECYCLE §18 and CHANGELOG. Verified: 307 lifecycle tests OK (33 in the features suite), CLI/machine suites OK, gates green (conventions, scope, complexity, LOC — cli.py back under 800), five non-vacuity guards re-proved. Refs #162 * test(lifecycle): feature ownership matrix - a user file is never adopted (#162) * feat(forge): pure work-authority resolver, claim grammar and durable journal (#163) ADR-0018's local half: the harness observes remote facts, this layer records them deterministically and never decides by guessing. - ainative/forge.py: `resolve_observed_work_authority()` — pure (no network, no credentials, no writes, no push authorization). Priority: explicit reference, then harness declaration, then exactly one compatible observed candidate; otherwise WORK_AUTHORITY_UNAVAILABLE / _AMBIGUOUS / _MISMATCH. Only github.com and gitlab.com are read as providers; a self-hosted host is `unknown`, never inferred from its name; a fork is ambiguity, never "prefer origin". - ainative/claims.py: canonical identities (`<provider>:principal:<id>`, `<provider>:<kind>:<id>`), UTC-normalized timestamps (a naive one refuses), total winner ordering with CLAIM_CONFLICT for unorderable duplicates, and the ClaimAttempt journal under `.ai-native/state/claim-attempts/` — PENDING written durably before the remote POST, outcomes CONFIRMED/LOST/CONFLICT/UNCERTAIN/ABANDONED, an unwritable or corrupt journal fails closed, abandon is an explicit local operator transition that keeps the record, and an abandoned record is final. - ainative/claim_cli.py + `ainative claim-attempt list|inspect|abandon --confirm`: the recovery surface. No retry path exists by design. - New refusal codes: WORK_AUTHORITY_*, CLAIM_INVALID, CLAIM_CONFLICT, CLAIM_UNCERTAIN, CLAIM_JOURNAL_UNAVAILABLE. - Tests: 27 in tests/test_forge_claims.py, including the invariant that a pending attempt survives a lifecycle update byte-identically and the CLI confirmation flow. CI runs them in the lifecycle job. Verified: 308 lifecycle tests OK, knowledge/machine/CLI suites OK, gates green (conventions, scope, complexity, LOC). Refs #163 * docs(workflow): provider-neutral FORGE-WORKFLOW, GitHub and GitLab mappings (#163) * docs(policy): distribute a provider-neutral AGENTS.md template (#163) The engineering-method component installs templates/AGENTS.md (Work Authority vocabulary, ADR-0018) instead of the repository's own AGENTS.md, which stays GitHub-specific. Identical to the root file except the work-management section; the fixture distribution ships both files. * feat(forge): forge detection, status observation and the doctor extension (#164) `ainative forge detect|status` observe the project's Git remotes and render the Work Authority resolution. Read-only by construction: the only subprocess is `git remote`, the only inputs are the pure resolver, the install state and the claim journal — zero network, zero credentials, zero writes, zero persistent trust. A fork renders as AMBIGUOUS (both candidates shown, no preference for origin) and an unknown host as UNAVAILABLE, both exit 0: detection is a diagnostic, the refusal belongs to the mutation path. - ainative/observation.py: remote reading, `forge_picture()` (resolution as state), `project_view()` (features + forge + unresolved claim attempts) and the doctor text lines. An unreadable claim journal is displayed, not a crash. - ainative/forge_cli.py: the command surface; `status` adds the effective feature set (shared projection) and the unresolved claim count. - Doctor extension: profile/features/forge/claim summary in both outputs, never a credential (`doctor --json` gains features, forge, claim_attempts). - status.py: reports the effective feature set through the same projection (`features`, `features_projected_from_legacy`), closing the read-only parity requirement for `status`. - ainative/cli_support.py: the small shared plumbing (emit/project/report/ plan text) extracted so cli.py stays the dispatcher under its LOC budget and the command modules stop importing render callables through cli.py. - Tests: 7 new observation tests (resolution, fork rendering, unknown host, features+claims in status, byte-identical project across detect/status/ doctor) and a status/projection parity test for a V1 state. Verified: 309 lifecycle tests OK, 139 CLI/knowledge/machine suites OK, gates green (conventions, scope, complexity, LOC: cli.py 757). Refs #164 * feat(release): one source resolver with fail-closed selector conflicts (#165) PR-4A. `resolve_release_source()` (ainative/lifecycle/release_source.py) is now the only function that decides where a release comes from, and `update`, `update check`, `status` and `doctor` all consume it — so the answer cannot differ between commands (ADR-0019 sections 1-3). - Selector rules exactly as frozen: local pair -> mirror; LOCAL_DIR without provider=local -> UPDATE_SOURCE_CONFLICT; URL mixed with any selector -> UPDATE_SOURCE_CONFLICT; URL alone -> anonymous; named provider -> machine config or built-in; nothing -> machine default_provider, else GitHub.com. Validation before precedence: nothing is ordered silently, and the old "unknown provider" refusal is preserved. - Machine scope `~/.ai-native/release-providers.json` (schema 1): reserved names (github/gitlab/local) cannot be redefined, a named provider is anonymous in V1 (a custom auth_origin is refused), a future schema refuses rather than guessing. `default_provider: local` requires a configured directory. New refusal codes: UPDATE_SOURCE_CONFLICT, RELEASE_CONFIG_INVALID. - `provider.build()` delegates to the resolver; the selector constants are re-exported so existing imports keep working. - Observability: `status` and `doctor` display the effective source, the selection reason, authenticated yes/no and the auth origin — never a secret (`describe()` turns a selector conflict into a displayed state, so the diagnostics keep diagnosing while the updater refuses). - Tests: 19 in tests/test_release_source.py (selector matrix, machine config, provider construction, status/doctor parity); CI runs them in the lifecycle job. Verified: 309 lifecycle tests OK, 123 update/CLI suites OK, gates green (conventions, scope, complexity 0 findings after splitting the resolver, LOC 0 warnings). Refs #165 * feat(release): Release V3 contract, anchored manifest and exact version chain (#165) PR-4B + PR-4C. ainative/lifecycle/release_v3.py owns the V3 vocabulary and the fail-closed policies around it (ADR-0019 sections 7-10): - Candidate policy: installable versions are SemVer without build metadata (1.2.3, 1.2.3-rc.1); `1.2.3+build1` refuses as RELEASE_BUILD_METADATA_UNSUPPORTED; ordering uses version precedence, never string order; two identities on one version refuse as RELEASE_DUPLICATE_VERSION (never "first match"); an enumeration that stopped at its bounds refuses as RELEASE_ENUMERATION_INCOMPLETE; a complete channel with nothing refuses as RELEASE_NO_CANDIDATE. - The external anchor travels WITH the candidate (provider metadata): manifest sha256 + size are verified BEFORE the document is parsed — RELEASE_INTEGRITY_METADATA_MISSING / _INVALID for absent/malformed anchors, UPDATE_INTEGRITY_FAILED for any mismatch. A hostile manifest can never influence whether its own bytes are trusted. - ReleaseManifest V3: schema, protocol, version, channel, compatibility, artifacts, provenance — strictly validated (plain filenames, exact one lifecycle artifact, canonical versions). A newer protocol refuses as CLI_UPDATE_REQUIRED, mirroring the bridge; anything else is an integrity refusal. - The exact version chain: candidate == manifest == compatibility.runtime_version == artifact == artifact filename == lifecycle-protocol.json release_version, with the broken link named in the refusal detail. `resolve_manifest()` formalizes the order: enumerate -> select -> verify -> parse -> chain. Tests: 32 in tests/test_release_v3.py (SemVer policy, selection, anchors, manifest refusals, every broken chain link, resolution order with a fake provider). CI runs them in the lifecycle job. Verified: 309 lifecycle tests OK, complexity 0 findings, LOC 0 warnings, scope within tolerance. Refs #165 * feat(release): V3 providers - GitHub, anonymous document, local mirror (#165) PR-4D + PR-4E. Each provider implements the `release_v3` contract and nothing else: enumerate, fetch the manifest, fetch an artifact. Every trust decision stays in release_v3 — the provider that fetched a manifest never decides whether to believe it (ADR-0019 sections 9-11). - GitHubReleaseProvider: bounded enumeration of the releases API (a full page says `complete=False`, so selection refuses instead of picking the best of what it saw); drafts, non-SemVer tags and releases without an `ainative-release-v3.json` asset are not candidates (a V2-era release is not a broken V3 one); the manifest asset's digest/size travel as the candidate's anchor; artifacts and manifests are fetched through the PR-0A transport (asset API locator preferred, `browser_download_url` fallback, octet-stream, bearer only at `api.github.com`). - AnonymousReleaseApiProvider: `AINATIVE_UPDATE_URL` as one GitHub-shaped release document, never authenticated; a document without a V3 manifest yields no candidate (`RELEASE_NO_CANDIDATE`), never a silent V2 fallback. - LocalReleaseProvider: `releases.json` channels carry the manifest locator with its size and SHA-256; the mirror executes the same logical chain as a network source, traversal-checked, bounded, and never trusted for being on disk. A V2-era channel entry is not a candidate. - `ReleaseCandidate` gains `manifest_locator` (provider-internal, opaque to the contract); `provider.environment_token()` exposes the environment token publicly for the V3 providers under the same origin rule. Tests: 16 in tests/test_release_providers.py, including an end-to-end `resolve_manifest` against the local mirror and against a scripted GitHub server, a tampered-manifest refusal, a bounded-listing refusal and a path-traversal refusal. CI runs them in the lifecycle job. Verified: 309 lifecycle tests OK, 67 release tests OK, complexity 0 findings, LOC 0 warnings, scope within tolerance. Refs #165 * feat(release): wire the updater to the V3 flow with a V2-era fallback (#165) PR-4 completion. `update check` and `update` now resolve through `resolve_v3_candidate()`: the resolved source is enumerated (bounded), the newest V3 candidate of the channel is selected, and the manifest is fetched and anchor-verified only when an update is applied — a check needs the version, nothing more. - A source that hits its enumeration bounds refuses (RELEASE_ENUMERATION_INCOMPLETE): partial results are never a reason to change generation. - A complete channel with no V3 candidate is a V2-era mirror, and the caller speaks the existing V2 path to it (deterministic, disclosed fallback — the bridge-era releases stay consumable). Everything else propagates. - apply(): V3 fetch verifies the artifact against the manifest's own size+SHA-256 before extraction, then the existing transaction applies the payload; the V3 bundle declares lifecycle protocol 3 (`_distribution_root` gained an `expected_protocol`), and a bundle carrying the V2 protocol refuses as UPDATE_VERSION_MISMATCH. - `provider.build_v3()` is the second patchable seam; the four tests that stub the V2 provider now stub it explicitly. Tests: 5 new end-to-end tests on a local V3 mirror (check, apply + rollback, tampered manifest refused with zero writes, protocol mismatch refused, CLI report). 314 lifecycle tests OK; release/CLI suites OK; gates green. Refs #165 * feat(release): GitLab provider, auth scheme per endpoint, purity tests (#166) PR-5 core. The GitLab provider implements the V3 contract (ADR-0019 §11): - Releases API for discovery only; the Generic Package Registry is the canonical distribution and integrity surface. Release Link URLs are never integrity roots — the anchor is the package file API's `file_sha256` and `size` (absent -> RELEASE_INTEGRITY_METADATA_MISSING, malformed -> RELEASE_INTEGRITY_METADATA_INVALID), and the manifest file lookup requires exactly one `ainative-release-v3.json` (0 -> RELEASE_MANIFEST_MISSING, >1 -> RELEASE_MANIFEST_AMBIGUOUS). - One project identity (`release_project_ref`) is used for Releases, packages and package files — no second project configuration. `release_project_ref` is a numeric id or namespace path, never a URL; it is machine-scope config (`providers.gitlab`), while `github`/`local` remain non-redefinable. - Exact package match (`type == generic`, canonical package name, selected version) with exactly one record, and bounded pagination (a full page refuses as incomplete, never a partial best-of). - Authentication shapes differ per provider: `ReleaseProviderEndpointConfig` gained `auth_header`/`auth_prefix`. GitLab sends `PRIVATE-TOKEN: <token>` from `GITLAB_TOKEN`, only at its configured origin; GitHub keeps `Authorization: Bearer`, unchanged. - GitLab is V3-only: `supports_v2_fallback = False`, and a complete channel without a V3 candidate refuses as RELEASE_NO_CANDIDATE instead of falling back to a generation that does not exist there. Tests: 10 GitLab provider tests on a scripted GitLab (exact-identity match, private-token confinement, duplicate packages, every manifest/integrity refusal, pagination bounds, full chain, anonymous endpoints) plus 3 config tests; and the three purity suites required by §74 (tests/purity/test_dependency_purity.py, test_contract_purity.py, test_distributed_policy_purity.py) — neutral modules do not import provider implementations, the neutral helpers behave identically for both forges, and the distributed policy carries no GitHub-only authority. Verified: 314 lifecycle tests OK, release suites OK, purity 7/7, complexity 0 findings, LOC 0 warnings. Refs #166 * docs(multiforge): support matrix, qualification report and READMEs (#165, #166) * fix(e2e): follow the distributed AGENTS.md template in the upgrade E2E (#165) * feat(security): mandatory secret patterns as an unremovable floor, scenario closures (#166) (#174) Closes the remaining traceable items from the Multi-Forge qualification report. - Candidate scanner (ainative/multivault/git_scanner.py): the constructor now takes `extra_secret_patterns` and no longer accepts a replacement set; the effective patterns are `MANDATORY_SECRET_PATTERNS + extras`, so no caller, repository or operator configuration can remove, replace, disable or shadow the floor (ADR-0019 section 12). The floor gains the documented GitLab token prefixes (`glpat-`, `gldt-`, `glrt-`, `glsoat-`), alongside private-key markers, AWS, GitHub and Slack patterns. The anti-debt owner (`finding_common.SECRET_PATTERNS`) and the vault-sync fallback carry the same GitLab prefixes. - Scenario J: the GitLab provider test proves an object-storage redirect on a package-file download is followed anonymously — the token stays at the API origin. - Scenario M: `doctor` warns when a GitLab remote is observed while the legacy `forge-github` default is only projected, naming the explicit `feature switch forge-gitlab` transition. - Scenario P: one end-to-end test asserts `update check`, `update`, `status` and `doctor` all surface the same `UPDATE_SOURCE_CONFLICT` for the same environment — one resolver, one refusal. - Documentation parity (plan section 75): `tests/purity/test_docs_parity.py` enforces the EN/FR README heading hierarchy, the operational surface (commands, env vars, config files) and the critical security statements — structurally, never by raw line counts. CI runs it with the purity suite. - Qualification report and CHANGELOG updated: scenarios J/M/P/Q and parity are GREEN; only the GitLab.com LIVE qualification and the release choreography remain, both declared. Verified: 314 lifecycle tests OK, 245 other suites OK (18 scanner, 110 release/claims/purity), gates green. Refs #166 * docs(qualification): GitLab.com qualified live (V3 chain, exact-runtime gate) (#166) (#175)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this brings
The complete Multi-Forge implementation (frozen architecture: Multi-Forge
v1.3.2-final; ADR-0017/0018/0019), on
multiforge, proposed fordev— notmain(per the maintainer instruction:mainis untouched until the fullimplementation is tested and integrated).
13 commits, ~7 000 changed lines, 314 lifecycle tests + 221 other suites all
green locally.
docs/MULTIFORGE-QUALIFICATION.mdstates exactly what wasverified and what remains.
AGENTS.mdtemplate (27 tests)Scope
transporthardening extensions,release_source,release_v3,release_providers,features,feature_cli,cli_support,forge,claims,claim_cli,observation,forge_cli,tests/purity/*.UPDATE_SOURCE_CONFLICT,RELEASE_CONFIG_INVALID, feature/claim/authority families.docs/FORGE-WORKFLOW.md(neutral policy),docs/GITLAB-WORKFLOW.md,refactored
docs/GITHUB-WORKFLOW.md,docs/MULTIFORGE-QUALIFICATION.md,templates/AGENTS.md(provider-neutral distributed policy), READMEs,SUPPORT.md, CHANGELOG.
Non-scope
second lifecycle transaction system.
Verification
suites — all green; gates green (conventions, scope, complexity 0 findings,
LOC 0 warnings).
"CI at the end" run requested by the maintainer.
qualification (no live project available), scenario Q mandatory secret
patterns (anti-debt scanner contract), EN/FR docs-parity automation, and two
partial E2E scenarios (J, M, P) — see the qualification report.
Risk
forge-githubcompatibilitydefault and the V2 update path keeps working (fallback proven by the existing
suite); selectors that used to be ignored now fail closed
(
AINATIVE_UPDATE_URL+ provider/local →UPDATE_SOURCE_CONFLICT;AINATIVE_UPDATE_LOCAL_DIRalone → conflict) — documented as breaking.(State V2 migrates state-last inside normal mutations), and no release has
been published from this branch.
Release choreography after merge
Still pending by design: publish the V2 bridge release containing PR-0B, then
the first V3 publication is gated by
V3_BRIDGE_RELEASE(plan §92).mainisnot touched by this PR.