Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,28 @@ jobs:
- name: Ownership, user modifications and external config
run: python -m unittest tests.test_lifecycle_ownership -v

- name: Features, State V2 and the shared projection
run: python -m unittest tests.test_lifecycle_features -v

- name: Work authority, claim grammar and the claim journal
run: python -m unittest tests.test_forge_claims -v

- name: Release source resolution and diagnostics
run: python -m unittest tests.test_release_source -v

- name: Release V3 contract, manifest and version chain
run: python -m unittest tests.test_release_v3 -v

- name: Release V3 providers (GitHub, anonymous, local mirror)
run: python -m unittest tests.test_release_providers -v

- name: Release V3 update path end to end
run: python -m unittest tests.test_lifecycle_update_v3 -v

- name: Purity - neutral layer, contracts, distributed policy
run: |
python -m unittest tests.purity.test_dependency_purity tests.purity.test_contract_purity tests.purity.test_distributed_policy_purity -v

- name: Path traversal, link escape, tampered state, archive safety
run: python -m unittest tests.test_lifecycle_security -v

Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,36 @@ project adheres to [Semantic Versioning](https://semver.org/).
workflow verifies a declared `V3_BRIDGE_RELEASE` through the V2 selection
rules before publishing a V3 release; a missing, unpublished or bundle-less
bridge blocks the release.
- **Features: profiles and capabilities are independent** (#162, ADR-0017):
`ainative feature enable | disable | switch | status` manages optional
project-scope capabilities in one lifecycle transaction. `forge-github` is
the default; it conflicts with `forge-gitlab`; a project has at most one
work forge, and `feature switch none` is the Generic Git shape. State schema
V2 records `active_features`; a V1 state projects to the compatibility
default and migrates inside the next mutation without resurrecting an
absent managed file.
- **Multi-Forge release sources and Release V3** (#165, ADR-0019): one
`resolve_release_source()` with fail-closed selector conflicts and a
machine-scope `~/.ai-native/release-providers.json`; a V3 release manifest
whose SHA-256 and size are verified (from provider metadata) *before* the
document is parsed; an exact version chain across candidate, manifest,
runtime, artifact, filename and the lifecycle protocol document; bounded
enumeration; SemVer without build metadata; duplicates refused. GitHub.com,
anonymous and local providers are wired into `update`/`update check`
(V2-era sources keep working), and `status`/`doctor` show the effective
source, its reason and whether it is authenticated.
- **GitLab provider (Release V3)** (#166): Releases for discovery, the
Generic Package Registry as the canonical integrity surface (`file_sha256`
+ `size`), exact package lookup, bounded pagination, and `PRIVATE-TOKEN`
confined to the configured origin. GitLab.com live qualification is
UNTESTED and therefore not declared supported —
`docs/MULTIFORGE-QUALIFICATION.md` states exactly what was verified.
- **Work Authority observation and claims** (#163/#164, ADR-0018):
`ainative forge detect | status` (zero network, credentials, writes or
persistent trust), the doctor extension, the pure local resolver, the
canonical claim grammar and the durable
`ainative claim-attempt list | inspect | abandon` journal — written before
the remote signal, never retried blindly.

### Security

Expand Down
22 changes: 22 additions & 0 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,28 @@ ainative doctor # inclut la section Knowledge
- Guides : docs/knowledge/KNOWLEDGE-CONVERGENCE-MATRIX.md et
docs/knowledge/K1-K4-MEASUREMENT-GATE.md

## Multi-Forge

Generic Git est le produit de base ; GitHub et GitLab sont des capacités
optionnelles, et le profil de gouvernance est indépendant d'elles
(ADR-0017/0018/0019).

```bash
ainative feature status # l'ensemble effectif de features
ainative feature switch forge-gitlab # une transaction : GitHub -> GitLab
ainative feature switch none # Generic Git : aucune forge de travail
ainative forge status # remotes observés + résolution Work Authority
ainative claim-attempt list # tentatives de claim journalisées (recovery)
```

Les sources de release sont résolues par une seule fonction aux sélecteurs
fail-closed (GitHub.com par défaut ; un miroir local ; une
`AINATIVE_UPDATE_URL` anonyme ; GitLab.com via un `release-providers.json` de
portée machine). La gestion du travail est provider-neutre
(`docs/FORGE-WORKFLOW.md`, avec les mappings GitHub et GitLab) ; la chaîne V3
vérifie un manifeste ancré de l'extérieur avant tout parsing, et la matrice de
support est dans `SUPPORT.md`.

## Démarrage rapide

### Installer sur un projet existant
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,26 @@ ainative doctor # includes the Knowledge section
- Guides: docs/knowledge/KNOWLEDGE-CONVERGENCE-MATRIX.md and
docs/knowledge/K1-K4-MEASUREMENT-GATE.md

## Multi-Forge

Generic Git is the base product; GitHub and GitLab are optional capabilities,
and the governance profile is independent of them (ADR-0017/0018/0019).

```bash
ainative feature status # the effective feature set
ainative feature switch forge-gitlab # one transaction: GitHub -> GitLab
ainative feature switch none # Generic Git: no work forge
ainative forge status # observed remotes + Work Authority resolution
ainative claim-attempt list # journaled claim attempts (recovery)
```

Release sources are resolved by one function with fail-closed selectors
(GitHub.com by default; a local mirror; an anonymous `AINATIVE_UPDATE_URL`;
GitLab.com through a machine-scoped `release-providers.json`). Work
management is provider-neutral (`docs/FORGE-WORKFLOW.md`, with the GitHub and
GitLab mappings); the V3 release chain verifies an externally anchored
manifest before parsing anything, and the support matrix is in `SUPPORT.md`.

## Quick Start

### For an existing project
Expand Down
3 changes: 3 additions & 0 deletions SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
| Operating systems | Linux, macOS and Windows (CI exercises all three) |
| Profiles | Standard; Verified (adds governed Work Contracts and deterministic verification) |
| Harnesses | Claude Code, Codex, OpenCode, Cursor, Gemini CLI, MiniMax/Mavis — for the shared method, skills and hooks |
| Work management | Generic Git; GitHub (mapping); GitLab (mapping). The Work Authority is resolved locally, purely and fail-closed (ADR-0018) |
| Features | Project-scope features independent of the profile (`forge-github` default, `forge-gitlab`), State schema V2 (ADR-0017) |
| Release sources | GitHub.com (built-in, qualified); a local mirror; an anonymous HTTPS release API (`AINATIVE_UPDATE_URL`, never credentialed); GitLab.com provider implemented and contract-tested, live-service qualification UNTESTED — **not declared supported**; GitLab Self-Managed and GitHub Enterprise Server UNTESTED — **not supported** |

Only the latest release is supported; fixes ship as a new patch version.

Expand Down
76 changes: 76 additions & 0 deletions ainative/claim_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""The `ainative claim-attempt` recovery surface: list, inspect, abandon.

Recovery is an operator act, so the surface is read-mostly: listing and
inspecting never change anything, and abandoning is one explicit transition
that requires `--confirm`. There is deliberately no "retry" here — an
uncertain POST is resolved by searching the remote for the attempt marker,
never by posting again (ADR-0018 section 6).
"""

from __future__ import annotations

import argparse
from pathlib import Path

from . import claims
from .cli_support import project_from, report as _report


def add_claim_parser(commands) -> None:
claim = commands.add_parser(
"claim-attempt", help="Inspect or abandon journaled claim attempts.")
subcommands = claim.add_subparsers(dest="claim_command", required=True)
listing = subcommands.add_parser("list", help="Every journaled claim attempt.")
_common(listing)
inspect = subcommands.add_parser("inspect", help="One attempt, in full.")
inspect.add_argument("attempt_id")
_common(inspect)
abandon = subcommands.add_parser(
"abandon", help="Give up an unresolved attempt (local transition only).")
abandon.add_argument("attempt_id")
abandon.add_argument("--confirm", action="store_true",
help="required: this is an operator decision")
_common(abandon)


def _common(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--project", type=Path, default=None,
help="project root (default: the current directory)")
parser.add_argument("--json", action="store_true", help="machine-readable output")


def _attempt_text(attempt: claims.ClaimAttempt) -> str:
lines = [f"{attempt.attempt_id}: {attempt.state}",
f" authority: {attempt.authority.get('provider')}:"
f"{attempt.authority.get('project')}",
f" item: {attempt.item}",
f" principal: {attempt.principal}",
f" marker: {attempt.marker}",
f" created: {attempt.created_at}"]
if attempt.outcome_at:
lines.append(f" outcome: {attempt.outcome_at}")
if attempt.note:
lines.append(f" note: {attempt.note}")
return "\n".join(lines)


def run_claim_command(args: argparse.Namespace) -> int:
project = project_from(args)
if args.claim_command == "list":
attempts = claims.list_attempts(project)
record = {"attempts": [attempt.to_record() for attempt in attempts],
"unresolved": [attempt.attempt_id for attempt in attempts
if attempt.state in claims.UNRESOLVED]}
lines = [f"{len(attempts)} attempt(s), {len(record['unresolved'])} unresolved"]
for attempt in attempts:
lines.append(f" {attempt.state:<10} {attempt.attempt_id} "
f"item {attempt.item}")
return _report(args, record, "\n".join(lines))
if args.claim_command == "inspect":
attempt = claims.load_attempt(project, args.attempt_id)
return _report(args, attempt.to_record(), _attempt_text(attempt))
attempt = claims.abandon(project, args.attempt_id, confirm=args.confirm)
return _report(args, attempt.to_record(), _attempt_text(attempt))


__all__ = ["add_claim_parser", "run_claim_command"]
Loading