diff --git a/.agnir/evidence/2026-09-03-agnir-v0.2.0-main-acceptance.md b/.agnir/evidence/2026-09-03-agnir-v0.2.0-main-acceptance.md new file mode 100644 index 0000000..cae6108 --- /dev/null +++ b/.agnir/evidence/2026-09-03-agnir-v0.2.0-main-acceptance.md @@ -0,0 +1,59 @@ +# Published Agnir v0.2.0 authoritative-main acceptance — 2026-09-03 + +Status: **accepted on authoritative Svif main; post-publication verification complete.** + +## Purpose + +Close the real downstream migration loop for Svif across the published Agnir compatibility boundary from `v0.1.1` / Core-profile `0.1` to `v0.2.0` / Core-profile `0.2`, and record the exact target-reconciliation/publication receipts. + +## Source and migration receipts + +- captured authoritative Svif main before migration: `dac058789a27f32f4ed1949874c1954f31f12bd8`; +- published source Agnir package: `v0.1.1` -> `e9712357ab590e5c1e5357b3cf3219d07d789aff`; +- published target Agnir package: `v0.2.0` -> `fc84095ed5d500be9e1b43a4af0e93356571bbd4`; +- validated migration source: `267f3d706e4fba67f2fb4a3a7ea33e80b9fb48ef`; +- migration source tree: `d6ffec2fddc48ec0052dd0531ca0088fb13b37b2`; +- final migration-source CI: run `33724859300`, repository-integrity/runtime-kernel/portable-contracts all success. + +The migration preserved Project identity `urn:svif:project:svif-core` and the existing State/Next/Decisions/Evidence locators while establishing an explicit Core 0.2 logical lineage distinct from its VCS selector. + +## Target reconciliation + +The validated source result was reconciled into target-owned main continuity before publication. + +- target-reconciled candidate: `2b5b92ab234d4c1b0d6596bbb0b8439eb6e05cfa`; +- target candidate tree: `191db90c0b959254025cb061159044c1b0ddf3d6`; +- first parent: captured main `dac058789a27f32f4ed1949874c1954f31f12bd8`; +- second parent: validated migration source `267f3d706e4fba67f2fb4a3a7ea33e80b9fb48ef`; +- target logical lineage: `urn:svif:lineage:authoritative`; +- target VCS selector: `refs/heads/main`; +- target Agnir operational provenance: `v0.2.0@fc84095ed5d500be9e1b43a4af0e93356571bbd4`. + +Candidate CI run `33725164044` passed repository-integrity, runtime-kernel and portable-contracts. PR #7's synthetic merge commit `1db24d60c7b4d60bde243c20fac1ab6ea1968798` produced exactly the candidate tree `191db90c0b959254025cb061159044c1b0ddf3d6`. + +Fresh stale checks immediately before publication confirmed: + +- main still `dac058789a27f32f4ed1949874c1954f31f12bd8`; +- migration source still `267f3d706e4fba67f2fb4a3a7ea33e80b9fb48ef`; +- integration ref still candidate `2b5b92ab234d4c1b0d6596bbb0b8439eb6e05cfa`. + +## Publication and observation + +Authoritative `main` advanced non-force exactly once from `dac058789a27...` directly to `2b5b92ab234...`. Ordinary PR merge was not used. No migration-line continuity was published first and repaired afterward. + +Post-publication push CI run `33725240001` completed successfully with all three jobs green: + +- `repository-integrity` — success; +- `runtime-kernel` — success; +- `portable-contracts` — success. + +Fresh authoritative-main reads after publication confirmed: + +- `AGNIR.yaml`: Core `0.2`, `repository-filesystem/0.2`, Project `urn:svif:project:svif-core`, lineage `urn:svif:lineage:authoritative`, selector `refs/heads/main`, unchanged durable locators, Agnir `v0.2.0@fc84095...`; +- `SVIF.yaml`: matching Project identity, compatibility/profile `0.2`, matching logical lineage and VCS selector. + +## Product evidence + +This real migration did not expose an Agnir `v0.2.0` semantic defect. The failures encountered during convergence were stale Svif guards that still encoded the previous current binding. Those guards were corrected while preserving Core/profile `0.1` regression coverage and the immutable `v0.2.0-preview.1` first-use bootstrap baseline. + +This receipt is materially stronger than both synthetic migration fixtures and the earlier pre-release Core 0.2 real-consumer validation because both endpoints are published Agnir releases and the accepted result is now authoritative Svif main truth. diff --git a/.agnir/evidence/2026-09-03-agnir-v0.2.0-real-downstream-migration.md b/.agnir/evidence/2026-09-03-agnir-v0.2.0-real-downstream-migration.md new file mode 100644 index 0000000..0061f2d --- /dev/null +++ b/.agnir/evidence/2026-09-03-agnir-v0.2.0-real-downstream-migration.md @@ -0,0 +1,72 @@ +# Published Agnir v0.1.1 -> v0.2.0 real downstream migration — 2026-09-03 + +Status: **validated downstream migration with target-main reconciliation staged; authoritative publication pending exact candidate validation and stale checks.** + +## Captured source Project + +- repository: `iorLab/svif`; +- captured authoritative main: `dac058789a27f32f4ed1949874c1954f31f12bd8`; +- Project identity: `urn:svif:project:svif-core`; +- source `AGNIR.yaml` blob: `3c94e5b5a342c3515f1ef67e3aec323029d64665`; +- source `SVIF.yaml` blob: `d7e360f747afa9f9844c6b722ef0b35b476b29a5`; +- source State blob: `d70d21e3ab8b29828b89edf38b514f91d12160d2`; +- source Next Actions blob: `486e09ab08821169b72ea7d6473abd2d383a20cd`; +- source Agnir repository release: `v0.1.1` -> `e9712357ab590e5c1e5357b3cf3219d07d789aff`; +- source Core/profile: `0.1` / `repository-filesystem/0.1`. + +The source already had durable State, Next Actions, Decisions, Evidence and one stable Project identity. This was therefore an explicit migration, never a clean re-initialization. + +## Target published Agnir compatibility + +- repository release: `v0.2.0`; +- exact stable tag target: `fc84095ed5d500be9e1b43a4af0e93356571bbd4`; +- Core compatibility: `0.2`; +- discovery profile: `repository-filesystem/0.2`. + +## Migration implementation sequence + +1. `ddaee058efe4c8381f60f5a2ebcae0de9ee9203d` added the already real-consumer-validated dual-line 0.1/0.2 Agnir adapter and tests without changing Project compatibility. +2. `eac2ab0dd70695d972b99afad084614eae26c77c` atomically migrated branch-local Project truth to Core/profile `0.2`, retained the Project identity and memory locators, introduced logical lineage `urn:svif:lineage:agnir-v0.2.0-stable-migration`, and separately bound selector `refs/heads/migration/agnir-v0.2.0-stable`. +3. Initial PR #6 run `33723726831` passed portable contracts; the only failures were Svif tests/checkers that still asserted the old current self-host binding plus one accidentally dropped distribution marker. No Agnir 0.2 adapter/runtime semantic defect appeared. +4. `8aaed18dbbbbb857873500505ae941289f0029c4` converged current-binding guards while preserving the immutable Preview.1 bootstrap Core/profile `0.1` contract. Run `33724143647` passed repository-integrity, runtime-kernel, and portable-contracts. +5. `5b2086bdc61cd5dad8397241565fbbda9592fc88` synchronized the active Skill and repository map. The Skill now explicitly distinguishes current Svif self-host Core/profile `0.2` from released Preview.1 first-use bootstrap Core/profile `0.1`, and validates Core 0.2 logical lineage plus applicable VCS selector binding. +6. Exact PR-head run `33724576017` passed all three jobs. +7. PR #6 synthetic merge `5d145ce1eb4ec4e6b837194a3e206b77bb71665b` produced tree `142051872a708c9944c737e1ebcee008ac27a381`, byte-identical to `5b2086...` tree. +8. `267f3d706e4fba67f2fb4a3a7ea33e80b9fb48ef` recorded the migration-line validation receipts without changing product semantics. Its exact tree is `d6ffec2fddc48ec0052dd0531ca0088fb13b37b2`; run `33724859300` passed repository-integrity, runtime-kernel, and portable-contracts. + +No Agnir `v0.2.0` product defect has been observed in this real downstream migration. The observed friction was confined to downstream guards and version-surface wording, and was repaired without weakening 0.1 regression/bootstrap pressure. + +## Product-boundary finding + +The migration established two independently versioned facts: + +1. the Svif repository Project's current self-host binding may consume published Agnir Core/profile `0.2`; +2. the already published `v0.2.0-preview.1` Plugin first-use bootstrap remains on its immutable Core/profile `0.1` baseline until a future Svif distribution deliberately changes onboarding. + +Conflating them would either leave the repository's current binding stale or retroactively rewrite a released distribution contract. + +## Target-main reconciliation staged + +Fresh reads before candidate construction observed: + +- authoritative main still at `dac058789a27f32f4ed1949874c1954f31f12bd8`; +- validated migration source still at `267f3d706e4fba67f2fb4a3a7ea33e80b9fb48ef`. + +The target candidate is constructed from the validated migration source product/package tree but deliberately replaces migration-line continuity with target-owned main truth: + +- Project identity remains `urn:svif:project:svif-core`; +- Core/profile remain `0.2` / `repository-filesystem/0.2`; +- stable Agnir operational provenance remains `v0.2.0` at `fc84095...`; +- authoritative logical lineage becomes `urn:svif:lineage:authoritative`; +- VCS selector becomes `refs/heads/main`; +- `SVIF.yaml` carries the same target lineage/selector binding; +- durable memory locators remain unchanged; +- target Current State / Next Actions preserve Svif Preview/distribution/authority obligations rather than copying migration-line State/Next verbatim. + +The migration source lineage, selector, and branch-local continuity are reconciliation input only. They are not target truth. + +## Publication invariant + +Main must remain at the captured target while the exact target-reconciled candidate is validated. Any synthetic validation tree must equal the staged candidate tree. Immediately before publication, both main and migration source refs must be re-read; any advance invalidates the candidate. Ordinary PR merge is not the publication primitive. + +Only after exact candidate validation and fresh stale checks may main advance once directly to the target-reconciled candidate. A subsequent authoritative-main run must prove fresh Core/profile `0.2` discovery, Project identity, target lineage/selector binding, preserved durable locators, stable Agnir provenance, and the complete Svif product check surface. A later checkpoint records the exact main receipt and closes the downstream-upgrade evidence loop. diff --git a/.agnir/next-actions.md b/.agnir/next-actions.md index 486e09a..3c24d74 100644 --- a/.agnir/next-actions.md +++ b/.agnir/next-actions.md @@ -1,72 +1,39 @@ # Svif Next Actions -1. **Retain the release branch until cleanup is explicitly confirmed.** Record and report its final remote tip after the post-release checkpoint reaches `main`; do not delete the local or remote branch without the Principal's confirmation. -2. **Continue the separate public/personal ChatGPT path.** Resolve or formally clarify the publisher-verification gate, then submit the exact tested Skills-only package to the universal Plugins Directory. Record scan/review evidence, explicitly Publish after approval, and validate the first individual-user ChatGPT Web installation without conflating it with the Repository Preview. -3. **Use a new Preview tag for any fix.** Keep `v0.2.0-preview.1` immutable; repair observed defects into `v0.2.0-preview.2` rather than moving the released tag. -4. **Repair only observed friction, then expand neutrality/surface evidence.** Keep `plugin/skills/svif/SKILL.md` single-sourced; add no MCP merely for publication. Add broader non-repository and multi-project evidence without making GitHub, Cloudflare, ChatGPT, Cursor, or another execution environment universal dependencies. -5. Keep live Cloudflare delivery disabled unless explicitly authorized. If authorized later, preserve exact verified-subject delivery and require independent observation before success claims. - -## Current Agnir compatibility reference - -- Agnir Core compatibility consumed by Svif: `0.1`. -- Repository/filesystem profile: `repository-filesystem/0.1`. -- Agnir repository release SemVer: stable `0.1.1`, formally published as `v0.1.1`. -- Target-main Svif Project operational provenance: `agnir-agent-skill` release `0.1.1` from `iorLab/agnir`, immutable applied revision `e9712357ab590e5c1e5357b3cf3219d07d789aff`. -- Current Agent-operable activation route: `Project root -> AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml -> declared durable memory`. -- Svif depends on Agnir continuity semantics/profile compatibility, not on Agnir's repository history, GitHub, or Skill repository at runtime. - -## Distribution and iteration rules - -- `plugin/` is an active product surface and the portable package targets Agent Plugins `1.0.0`. -- `plugin/skills/svif/SKILL.md` remains the shared Svif Project-orchestration workflow; product-specific packaging must reuse it rather than fork behavior. -- The initial public ChatGPT release is deliberately **Skills only**. Current OpenAI public submission accepts this shape directly; MCP/App packaging is not a publication prerequisite. -- `plugin/.codex-plugin/plugin.json` carries the current OpenAI/Codex public-listing metadata and points to the same `skills/` implementation. -- `.agents/plugins/marketplace.json` is the supported self-distributed `v0.2.0-preview.1` route for Codex CLI and ChatGPT desktop/Codex. It remains separate from universal-directory publication and personal ChatGPT Web onboarding. -- Repository CI validates package/conformance/distribution properties; do not call personal ChatGPT installation validated until an actual individual-user ChatGPT surface has installed and exercised the published Plugin. -- Publisher/account verification gates are external release constraints. They must not be mistaken for package/runtime failures or used as justification for speculative MCP/App changes. -- Plugin changes SHOULD be driven by real submission, installation, or execution friction whenever possible. -- **First-use onboarding is a Svif product responsibility.** When the selected Project is genuinely uninitialized and no durable binding chooses another Continuity Provider, Svif's founding repository/filesystem path establishes Agnir Core `0.1` / `repository-filesystem/0.1` continuity plus a matching minimal `project-binding/0.2` `SVIF.yaml` using one stable Project identity. A user MUST NOT have to pre-initialize Agnir as a prerequisite for first Svif use. -- Partial/broken Agnir/Svif artifacts are repair cases, not clean bootstrap cases. An intentionally configured different Continuity Provider must not be overwritten with Agnir. -- The first-use bootstrap consumes Agnir protocol/profile semantics through the founding Continuity Provider integration and must not make the Agnir Skill repository, prior installation chat, GitHub, or another execution surface a runtime prerequisite. -- Distribution MUST NOT reimplement `src/svif/runtime.py`, move Project truth out of the Continuity Provider, or grant protected authority through model-controlled payloads. -- ChatGPT Web availability is a product requirement for the current personal-user target. Any future packaging restriction that removes Web support must be surfaced as a deliberate product decision, not hidden as an implementation detail. - -### Auxiliary repository-marketplace evidence rule - -The GitHub marketplace path remains a useful secondary validation channel. **Package/conformance/distribution CI is not installation evidence.** For a revision-sensitive marketplace exercise, record the immutable commit SHA actually invoked when the client exposes enough evidence to establish it. Treat a repository ref's current SHA only as comparison evidence; when the surface cannot bind invocation to one immutable commit, preserve exact installed-revision provenance as unconfirmed. - -## Documentation maintenance rule - -- Architecture/runtime/distribution changes are incomplete until affected explanatory sections and diagrams in both `README.md` and `README.zh-CN.md` are updated in the same change set. -- Localized diagrams are comprehension-first rather than literal translations; important Simplified Chinese nodes explain both role and responsibility. -- README repository trees remain compact navigation views. -- `REPOSITORY_TREE.md` is the exhaustive file-level map. Tracked file additions/removals/moves or material responsibility changes must update it in the same change set; if the compact tree is also affected, both README language versions must update as well. - -## Branch governance - -- `main` is the only long-lived branch. -- Historical predecessor and retired work is indexed by immutable commit SHA in `history/BRANCH_ARCHIVE.md`; live legacy/feature/fix/tmp branch refs are not retained. - -## Completed in the current implementation sequence - -- Product Architecture `0.2` frozen around Orchestrator + Continuity Provider + Execution Surface + Capability Provider. -- Minimal executable Orchestrator implemented and CI-proven. -- Concrete Agnir repository/filesystem Continuity Provider implemented. -- ChatGPT structured Execution Surface bridge implemented with `begin()` / `complete()` handoff. -- Cloudflare provider ownership consolidated into `src/svif/capabilities/cloudflare.py` and `integrations/cloudflare/`. -- English and Simplified Chinese README entry points include synchronized Architecture and Runtime / Operation Flow diagrams. -- Founding credential-free E2E implemented at `tests/test_founding_e2e.py`. -- Skill-first Plugin MVP exists under `plugin/` using Agent Plugins `1.0.0` packaging. -- OpenAI/Codex repository marketplace metadata maps to the same Skill-first Plugin root without duplicating runtime semantics. -- The personal ChatGPT audience/distribution correction is recorded in `.agnir/evidence/2026-08-31-personal-chatgpt-distribution-checkpoint.md`. -- Current OpenAI public submission requirements have been re-verified: Skills-only public Plugins are accepted; the repository manifest, README guidance, listing metadata, and review-case preparation have been aligned to that route. -- A real publisher verification attempt reached the individual-developer verification flow but was blocked at the required accepted-default-payment-method gate before verification/submission. This is recorded as an external release blocker without storing private payment/account data. -- Svif first-use onboarding now handles an ordinary non-Agnir Project without requiring manual Agnir pre-initialization; regression pressure is `tests/test_plugin_first_use_bootstrap.py` and durable evidence is `.agnir/evidence/2026-08-31-plugin-first-use-bootstrap-fix.md`. -- The target-main candidate upgrades Svif's Agnir operational baseline compatibly to stable `v0.1.1`; `AGNIR.yaml` records immutable provenance without changing Core/profile compatibility, Project identity, memory locators, or `SVIF.yaml`. -- The immutable candidate passed Codex CLI and ChatGPT desktop/Codex installation, bootstrap, checkpoint, initialized-Project idempotency, and fresh-context recovery acceptance before release. -- Svif `v0.2.0-preview.1` is released from authoritative `main` as a GitHub Prerelease; immutable tag installation resolves to the verified release commit and exposes the Skills-only Plugin as installed/enabled. -- Main-only branch governance is complete. - -## Repository-retirement note - -The former `iorLab/svif-cloudflare-reference` project is retired. No future Svif work should target it. +The real Svif Project migration from published Agnir `v0.1.1` / Core-profile `0.1` to published stable Agnir `v0.2.0` / Core-profile `0.2` has been accepted on authoritative `main` through target-owned reconciliation. Main now uses logical lineage `urn:svif:lineage:authoritative` bound separately to `refs/heads/main`, and authoritative push CI is green. + +1. **Feed the completed real downstream upgrade into Agnir v1 evidence.** Record the exact Svif pre-migration main, migration source, target-reconciled candidate, candidate tree, source/candidate/main CI runs, synthetic-tree receipts, fresh stale checks, main publication boundary, Project identity/locator preservation, and the distinction between current self-host Core/profile `0.2` and immutable Preview.1 bootstrap Core/profile `0.1`. +2. **Preserve the released Repository Preview and distribution evidence.** Keep `v0.2.0-preview.1` immutable and preserve its **immutable candidate**, real **Codex CLI**, and **ChatGPT desktop/Codex** acceptance evidence. Any Preview fix uses a new tag such as `v0.2.0-preview.2`. +3. **Continue the separate public/personal ChatGPT path when the publisher gate is resolvable.** Submit the supported Skills-only package to the **universal Plugins Directory**, explicitly Publish after approval, then validate a real **individual-user ChatGPT surface**, with **ChatGPT Web** remaining a first-class target. +4. **Treat future Agnir updates according to compatibility semantics.** Core `0.2` compatible repository updates may preserve the current lineage; any future incompatible Core boundary must use explicit migration rather than silent upgrade. +5. Keep live Cloudflare delivery disabled unless explicitly authorized. +6. Retire temporary migration/validation refs after their evidence has been safely captured and when a safe delete-ref path is available. `main` remains the only long-lived Svif branch. + +## Completed published-Agnir migration receipts + +- captured pre-migration main: `dac058789a27f32f4ed1949874c1954f31f12bd8`; +- published Agnir source: `v0.1.1` -> `e9712357ab590e5c1e5357b3cf3219d07d789aff`; +- published Agnir target: `v0.2.0` -> `fc84095ed5d500be9e1b43a4af0e93356571bbd4`; +- validated migration source: `267f3d706e4fba67f2fb4a3a7ea33e80b9fb48ef`, tree `d6ffec2fddc48ec0052dd0531ca0088fb13b37b2`; +- migration source CI: `33724859300` success, all three jobs green; +- target-reconciled candidate/main publication revision: `2b5b92ab234d4c1b0d6596bbb0b8439eb6e05cfa`; +- target candidate tree: `191db90c0b959254025cb061159044c1b0ddf3d6`; +- candidate CI: `33725164044` success, all three jobs green; +- PR #7 synthetic merge: `1db24d60c7b4d60bde243c20fac1ab6ea1968798`, exact tree `191db90c...`; +- authoritative-main push CI: `33725240001` success, all three jobs green; +- Project identity: `urn:svif:project:svif-core`; +- authoritative logical lineage: `urn:svif:lineage:authoritative`; +- authoritative selector: `refs/heads/main`; +- durable State/Next/Decisions/Evidence locators: unchanged; +- main `AGNIR.yaml` and `SVIF.yaml`: Core/profile `0.2`, matching lineage/selector binding, Agnir operational `v0.2.0@fc84095...`. + +## Invariants + +- Project identity remains stable across migration. +- Durable State/Next/Decisions/Evidence remain Project-owned. +- logical lineage identity != VCS selector != commit/checkpoint receipt. +- Core `0.1` -> `0.2` is explicit migration, not compatible upgrade. +- Source/migration continuity is reconciliation input, not automatic target truth. +- Main publication exposed Project result + reconciled target continuity in one coherent ref advancement. +- The released Preview.1 bootstrap baseline and the Svif repository's current self-host binding are separate versioned facts. +- Svif product architecture remains Orchestrator + Continuity Provider + Execution Surface + Capability Provider. diff --git a/.agnir/state.md b/.agnir/state.md index d70d21e..726b323 100644 --- a/.agnir/state.md +++ b/.agnir/state.md @@ -1,115 +1,62 @@ # Svif Current State -Svif is the authoritative active **Project orchestration product** in `iorLab/svif` on `main`. Agnir is the independent founding Continuity Provider in `iorLab/agnir`. The former `iorLab/svif-cloudflare-reference` project is retired. Historical ZeroLocal material, retired branches, and the retired Cloudflare reference are evidence only and are not active dependencies or release gates. +Svif is the authoritative active **Project orchestration product** in `iorLab/svif`. Authoritative `main` has now accepted the validated published-Agnir migration result through target-owned continuity reconciliation. The former `iorLab/svif-cloudflare-reference` project is retired. ## Product architecture -Svif coordinates four first-class components: +Svif continues to coordinate the same four first-class components: Orchestrator (`src/svif/runtime.py`), Continuity Provider (`src/svif/continuity/agnir.py`), Execution Surface (`src/svif/execution/chatgpt.py`), and Capability Provider (`src/svif/capabilities/cloudflare.py`). The Project persists; Executors and execution environments may change. No execution surface becomes canonical Project truth merely because execution occurred there. -1. **Orchestrator** — `src/svif/runtime.py`; -2. **Continuity Provider** — founding Agnir implementation at `src/svif/continuity/agnir.py`; -3. **Execution Surface** — founding ChatGPT bridge at `src/svif/execution/chatgpt.py`; -4. **Capability Provider** — founding Cloudflare Workers provider at `src/svif/capabilities/cloudflare.py`. +## Released Svif product state -Stable rule: - -> The Project persists; Executors and execution environments may change. - -No execution surface becomes canonical Project truth merely because execution occurred there. - -## Active contracts and released Preview - -- Svif product line: `0.2`; the current released Repository Preview is `0.2.0-preview.1`. +- Svif product line: `0.2`. +- Released Repository Preview: immutable `v0.2.0-preview.1` from commit `2b07b6b5ea0bc8feee59f9f647be9af3069d056e`. - Project Binding: `project-binding/0.2`. - Software Delivery profile: `software-delivery/0.2`. - Capability Adapter: `capability-adapter/0.2`. - Evidence record: `evidence-record/0.2`. -- Repository/filesystem binding serialization: `SVIF.yaml`. -- Agnir Core compatibility: `0.1`. -- Agnir discovery profile: `repository-filesystem/0.1`. -- The target-main candidate applies Agnir stable repository release `0.1.1` from `iorLab/agnir`, immutable revision `e9712357ab590e5c1e5357b3cf3219d07d789aff`, as a compatible operational upgrade recorded in `AGNIR.yaml`. -- Canonical Agnir activation route: `Project root -> AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml -> declared durable memory`. -- Canonical Svif repository/ref: `iorLab/svif` / `main`. - -The Agnir `v0.1.1` upgrade is a **compatible operational upgrade**, not a migration: Core `0.1` and `repository-filesystem/0.1` are unchanged, while Project identity, memory locators/content, unrelated manifest extensions, and `SVIF.yaml` remain preserved. Its execution-surface activation handoff repair is directly relevant to the Repository Preview installation path. - -Svif `v0.2.0-preview.1` was released from authoritative `main` commit `2b07b6b5ea0bc8feee59f9f647be9af3069d056e`. Annotated tag object `2535cb89426c2d38c2e061948e81954a7c7c26d7` peels to that commit, and GitHub Release `RE_kwDOUEzlR84WteIK` is published as a non-draft Prerelease. Initial immutable candidate `1dbcce6582f218b0762fb655bc03455517c79802` passed real Codex CLI plus ChatGPT desktop/Codex installation, bootstrap, checkpoint, initialized-Project idempotency, and fresh-context recovery acceptance; the final evidence-only candidate preserved the Plugin tree, passed CI runs `33598513827` and `33598628565`, and passed a tag-based fresh installation smoke. The short-lived release branch remains a temporary evidence carrier only and is not a second continuity authority. - -## README entry architecture - -The README front section is now deliberately layered before architecture material: - -1. `Start Here` / `从这里开始` — minimal user actions for personal-ChatGPT availability, installation in compatible Agent environments, normal continuation, and upgrading the Agnir used by the Project; -2. `Agnir Project Instructions` — canonical Agent activation/operation guidance for this repository; -3. `What Svif Adds to a Project` / `Svif 会给 Project 增加什么` — concrete first-use Project surface, with `AGENTS.md` / `README.md` visibly marked as non-destructive EDIT/add-entry-only and `AGNIR.yaml` / `.agnir/` / `SVIF.yaml` as founding ADD surfaces; -4. `Architecture Diagram` / `架构图` — static product architecture plus the first-use boundary; -5. `Runtime / Operation Flow` / `运行流程` — post-bootstrap runtime behavior, intentionally free of install-mutation labels. - -A genuinely uninitialized Project does **not** require manual Agnir pre-initialization. The active Svif first-use contract remains that the shared Skill establishes founding Agnir continuity plus a matching minimal Svif Project Binding on the repository/filesystem path. Compatible existing artifacts are reused; partial/contradictory artifacts are repair cases; an intentional other Continuity Provider binding is preserved. - -Repository-integrity checks enforce the entry ordering, first-use ADD/EDIT distinction, architecture/runtime separation, and canonical user intents. Durable rationale is recorded in `.agnir/evidence/2026-09-01-readme-information-architecture.md` and `.agnir/decisions.md`. `README.md` and `README.zh-CN.md` remain synchronized entry points. - -## Runtime baseline - -- Externally driven execution uses `Orchestrator.begin()` / `Orchestrator.complete()`. -- Untrusted model/result payloads cannot self-grant protected authority. -- External actuation requires successful verification for the exact subject plus applicable trusted authority. -- External success requires independent observation before checkpoint. -- Agnir durable continuity remains Project-owned and execution-surface-neutral. -- Detached commits, PR checkouts, temporary branches, forks, mirrors, or other non-authoritative copies do not silently become canonical checkpoint targets. - -`tests/test_founding_e2e.py` proves the credential-free orchestration loop through Agnir continuity load -> Orchestrator -> ChatGPT bridge -> trusted authority -> exact-subject Cloudflare actuation through injected fake transport -> independent observation -> Agnir checkpoint -> continuity reload/resume. This is not live Cloudflare production-delivery evidence. - -## Plugin MVP and first-use onboarding - -Svif has a released **Skills-only `v0.2.0-preview.1` Plugin Preview** under `plugin/`: +- The released **Plugin MVP** / Preview.1 first-use bootstrap remains on its published Agnir Core/profile `0.1` baseline until a later Svif distribution intentionally changes that onboarding contract. +- The real **Codex CLI** and **ChatGPT desktop/Codex** Preview acceptance evidence remains valid and the released tag remains immutable. +- The separate public/personal ChatGPT path remains externally blocked at publisher verification/payment-method eligibility; repository CI is not personal **ChatGPT Web** installation evidence. +- Live Cloudflare delivery remains disabled unless explicitly authorized. +- `README.md` and `README.zh-CN.md` remain synchronized user/Agent entry points. -- `plugin/plugin.json` — portable Agent Plugins `1.0.0` manifest; -- `plugin/skills/svif/SKILL.md` — shared Svif Project-orchestration Skill; -- `plugin/.codex-plugin/plugin.json` — OpenAI/Codex manifest reusing the same Skill and carrying public-listing metadata; -- `.agents/plugins/marketplace.json` — repository-backed Preview catalog for Codex CLI and ChatGPT desktop/Codex; -- `plugin/README.md` — submission, installation, review-case, and evidence-boundary guidance. +## Authoritative Agnir compatibility — accepted 2026-09-03 -First-use onboarding is a Svif product responsibility. For a genuinely uninitialized ordinary Project, the shared Skill establishes one stable Project identity, Agnir Core `0.1` / `repository-filesystem/0.1` continuity, a matching minimal `project-binding/0.2` `SVIF.yaml`, then fresh-activates and continues the original task. Partial/broken Agnir/Svif artifacts remain repair cases, and a Project intentionally bound to another Continuity Provider is not overwritten with Agnir. +The Svif Project itself has completed an explicit migration from published Agnir `v0.1.1` / Core-profile `0.1` to published stable Agnir `v0.2.0` / Core-profile `0.2`. -Repository checks can prove package/conformance, runtime, repository integrity, Agnir discovery guardrails, distribution metadata consistency, documentation claim boundaries, and first-use bootstrap regression behavior. They do **not** prove supported-client installation, OpenAI review, universal-directory publication, or personal ChatGPT invocation. +Authoritative continuity is now: -## Repository Preview distribution status +- Project identity: `urn:svif:project:svif-core` — unchanged; +- Agnir Core: `0.2`; +- profile: `repository-filesystem/0.2`; +- authoritative logical lineage: `urn:svif:lineage:authoritative`; +- VCS selector: `refs/heads/main`; +- durable locators: `.agnir/state.md`, `.agnir/next-actions.md`, `.agnir/decisions.md`, `.agnir/evidence/` — unchanged; +- Agnir operational release: `0.2.0`; +- immutable applied revision: `fc84095ed5d500be9e1b43a4af0e93356571bbd4`. -The copy-ready user intent remains exactly: +`SVIF.yaml` declares the same Project identity, Core/profile compatibility, logical lineage and VCS selector binding. Logical lineage identity is not derived from the branch ref or commit receipt. -`Install and enable Svif for this Project: https://github.com/iorLab/svif` +## Accepted migration and publication receipts -The installer owns fixed-tag resolution, marketplace registration, client-capability checks, first-use bootstrap, and evidence. The released Preview supports Codex CLI and ChatGPT desktop/Codex through the repository marketplace fixed to immutable tag `v0.2.0-preview.1`. Moving `main` is not a released Preview, and ChatGPT Web/mobile cannot install this repository Preview through the prompt alone. +Captured pre-migration authoritative main: `dac058789a27f32f4ed1949874c1954f31f12bd8`. -## Personal ChatGPT public distribution status +Validated migration source: `267f3d706e4fba67f2fb4a3a7ea33e80b9fb48ef`, tree `d6ffec2fddc48ec0052dd0531ca0088fb13b37b2`; exact source CI run `33724859300` passed repository-integrity, runtime-kernel and portable-contracts. -The primary ChatGPT audience is individual/personal users. The mature consumer path is: +Target-reconciled candidate: `2b5b92ab234d4c1b0d6596bbb0b8439eb6e05cfa`, tree `191db90c0b959254025cb061159044c1b0ddf3d6`, with first parent `dac058789a27...` and second parent `267f3d706...`. -`individual ChatGPT user -> universal Plugins Directory -> install -> invoke Svif in normal ChatGPT use` +Candidate validation run `33725164044` passed all three jobs. PR #7 synthetic merge `1db24d60c7b4d60bde243c20fac1ab6ea1968798` produced exactly the same tree `191db90c...` as the staged candidate. Fresh stale checks immediately before publication confirmed main, migration source and candidate refs had not advanced. -Svif is not publicly listed yet. The repository-side Skills-only package is aligned to the current OpenAI public-submission route, but the real publisher flow is externally blocked before individual developer verification because the Platform requires an accepted default payment method. This is an account/publisher eligibility blocker, not evidence of a Svif package, Skill, Orchestrator, or runtime defect. +Authoritative `main` then advanced exactly once, non-force, directly from `dac058789a27...` to `2b5b92ab234...`; ordinary PR merge was not used. There was no interval in which migration-line `AGNIR.yaml`, State or Next Actions were published as main truth and repaired afterward. -Do not weaken the Skills-only package, add MCP merely to escape this gate, invent billing identity, or claim review/publication/install success while publisher verification is blocked. ChatGPT Web remains a first-class target. MCP/App packaging is a later capability increment, not a release gate. +Post-publication main push CI run `33725240001` passed repository-integrity, runtime-kernel full unittest discovery and portable-contracts. Fresh reads of `main` confirm `AGNIR.yaml` and `SVIF.yaml` both resolve Core/profile `0.2`, Project `urn:svif:project:svif-core`, lineage `urn:svif:lineage:authoritative`, selector `refs/heads/main`, preserved durable locators and Agnir `v0.2.0@fc84095...` provenance. -## Current resume point +This is the first recorded real Svif Project upgrade across the **published** Agnir `v0.1.1` -> **published** `v0.2.0` compatibility boundary. It is distinct from synthetic migration fixtures and the earlier pre-release Core 0.2 real-consumer experiment. -1. Preserve the temporary release branch and its final tip until the Principal explicitly confirms cleanup; then delete the local and remote branch without changing the tag or released `main` history. -2. Keep the later public/personal ChatGPT submission path separate: resolve the publisher gate, submit the same Skills-only package, explicitly Publish after approval, then validate the universal Plugins Directory and personal ChatGPT Web. -3. If the Preview requires a fix, create `v0.2.0-preview.2`; never move the released `v0.2.0-preview.1` tag. -4. Keep live Cloudflare delivery disabled unless explicitly authorized. +## Evidence consequence -## Evidence checkpoints +The migration has not exposed an Agnir `v0.2.0` product defect. The only convergence failures encountered were stale Svif guards that still described the old current binding; they were repaired without weakening the retained Core/profile `0.1` regression and immutable Preview.1 onboarding baseline. -- Repository Preview release: `.agnir/evidence/2026-09-02-svif-v0.2.0-preview.1-release.md`; authoritative-main/tag identity, GitHub Prerelease, CI, and tag-based installation smoke are observed. -- Repository Preview candidate acceptance: `.agnir/evidence/2026-09-02-svif-v0.2.0-preview.1-candidate.md`; static checks, GitHub CI, and immutable-SHA Codex CLI plus ChatGPT desktop/Codex acceptance passed before release. -- Agnir `v0.1.1` compatible operational upgrade candidate: `.agnir/evidence/2026-09-02-agnir-v0.1.1-compatible-upgrade.md`; stable tag `e9712357ab590e5c1e5357b3cf3219d07d789aff`, Core/profile unchanged. -- README audience split / first-use Project surface: `.agnir/evidence/2026-09-01-readme-information-architecture.md`. -- Previous Agnir `v0.1.0` compatible operational upgrade: `.agnir/evidence/2026-09-01-agnir-v0.1.0-compatible-upgrade.md`; upgrade revision `c7cd42b6e94556a630570a54e22c72acc97f3ecf`, run `33466389590` success. -- Plugin first-use bootstrap fix: `.agnir/evidence/2026-08-31-plugin-first-use-bootstrap-fix.md`; final behavior baseline `b90d1f8976b0e03d2c5a3b70c9bbb4b032c37724`, run `33384858568` success. -- Personal ChatGPT distribution / publisher-gate checkpoint: `.agnir/evidence/2026-08-31-personal-chatgpt-distribution-checkpoint.md`. -- Plugin MVP hardening checkpoint: `.agnir/evidence/2026-08-31-plugin-mvp-hardening-checkpoint.md`. -- README/localization baseline: `.agnir/evidence/2026-08-28-readme-diagram-localization-checkpoint.md`. -- Founding E2E: `.agnir/evidence/2026-08-28-founding-e2e.md`, run `33143308949` success. +The next material action is to record these exact downstream-upgrade receipts in `iorLab/agnir` as v1 evidence, then continue the separate Svif distribution obligations. -`.agnir/decisions.md` is authoritative for architecture and distribution decisions; `.agnir/next-actions.md` is the canonical ordered resume plan. +`.agnir/next-actions.md` is the canonical ordered resume plan. diff --git a/AGNIR.yaml b/AGNIR.yaml index 3c94e5b..5b3be6d 100644 --- a/AGNIR.yaml +++ b/AGNIR.yaml @@ -1,12 +1,15 @@ agnir: - version: "0.1" - discovery_profile: "repository-filesystem/0.1" + version: "0.2" + discovery_profile: "repository-filesystem/0.2" project: identity: "urn:svif:project:svif-core" profiles: - generic +continuity: + lineage: "urn:svif:lineage:authoritative" + memory: state: ".agnir/state.md" next_actions: ".agnir/next-actions.md" @@ -20,8 +23,14 @@ extensions: agnir/repository: canonical: "iorLab/svif" authoritative_ref: "main" + agnir/vcs: + branch_continuity: "lineage-bound" + integration_reconciliation: "required" + lineage_binding: + kind: "vcs-ref" + selector: "refs/heads/main" agnir/operations: distribution: "agnir-agent-skill" - release: "0.1.1" + release: "0.2.0" source: "iorLab/agnir" - applied_revision: "e9712357ab590e5c1e5357b3cf3219d07d789aff" + applied_revision: "fc84095ed5d500be9e1b43a4af0e93356571bbd4" diff --git a/REPOSITORY_TREE.md b/REPOSITORY_TREE.md index 42ed039..ea73835 100644 --- a/REPOSITORY_TREE.md +++ b/REPOSITORY_TREE.md @@ -37,6 +37,8 @@ svif/ # Svif 产品主仓库 │ ├── 2026-09-02-agnir-v0.1.1-compatible-upgrade.md # Agnir v0.1.1 compatible operational upgrade 与 immutable provenance │ ├── 2026-09-02-svif-v0.2.0-preview.1-candidate.md # Preview 候选、CI、Codex CLI 与桌面端真实安装验收证据 │ ├── 2026-09-02-svif-v0.2.0-preview.1-release.md # tag、GitHub Prerelease、main CI 与 tag-based 安装烟测证据 +│ ├── 2026-09-03-agnir-v0.2.0-real-downstream-migration.md # Svif 从发布版 Agnir v0.1.1/Core 0.1 迁移到 v0.2.0/Core 0.2 的真实下游证据 +│ ├── 2026-09-03-agnir-v0.2.0-main-acceptance.md # target reconciliation、authoritative main publication 与 post-publication CI/cold-start 验证证据 │ └── checkpoint-2026-08-28-validation-2.md # Validation 2 的持久 checkpoint 记录 │ ├── .github/ # GitHub 托管侧自动化配置 @@ -49,7 +51,7 @@ svif/ # Svif 产品主仓库 │ ├── runtime.py # Orchestrator 核心:begin/run/complete、验证、权限、reconcile、checkpoint │ ├── continuity/ # Continuity Provider 实现 / 适配层 │ │ ├── __init__.py # continuity 子包入口 -│ │ └── agnir.py # 当前 founding provider:Agnir repository/filesystem continuity +│ │ └── agnir.py # founding Agnir repository/filesystem Continuity Provider;兼容 0.1 并支持当前 0.2 lineage/binding │ ├── execution/ # Execution Surface 桥接层 │ │ ├── __init__.py # execution 子包入口 │ │ └── chatgpt.py # 当前 founding surface:ChatGPT 结构化 begin/complete bridge @@ -89,7 +91,8 @@ svif/ # Svif 产品主仓库 │ ├── tests/ # 可执行产品实现测试 │ ├── test_runtime.py # Orchestrator kernel、authority、verification、lifecycle 行为 -│ ├── test_agnir_continuity.py # Agnir Continuity Provider adapter 的 load / checkpoint / failure 行为 +│ ├── test_agnir_continuity.py # Agnir Continuity Provider adapter 的 0.1/0.2 load / lineage / checkpoint / failure 行为 +│ ├── test_agnir_stable_migration.py # 当前 Svif Project 对发布版 Agnir v0.2.0 的 self-consumption、identity/locator/lineage 迁移 guard │ ├── test_chatgpt_surface.py # ChatGPT Execution Surface materialize / parse / identity 约束 │ ├── test_cloudflare_capability.py # Cloudflare provider 的 actuation / observation / subject-target 约束 │ ├── test_founding_e2e.py # founding Agnir + ChatGPT + Cloudflare 完整产品闭环 @@ -134,4 +137,4 @@ svif/ # Svif 产品主仓库 如果只是第一次理解 Svif,优先看 README 里的简略树即可;需要定位某个具体 contract、fixture、test、Plugin artifact、evidence 或 integration 文件时,再查本页。 -本页不是第二套架构定义。**架构语义仍以 `ARCHITECTURE.md`、`spec/`、`SVIF.yaml` 和 canonical Agnir decisions/state 为准;本页负责把这些职责映射回仓库中的实际文件位置。** +本页不是第二套架构定义。**架构语义仍以 `ARCHITECTURE.md`、`spec/`、`SVIF.yaml` 和 canonical Agnir decisions/state 为准;本页负责把这些职责映射回仓库中的实际文件位置。** \ No newline at end of file diff --git a/SVIF.yaml b/SVIF.yaml index d7e360f..ae1908c 100644 --- a/SVIF.yaml +++ b/SVIF.yaml @@ -8,11 +8,13 @@ project: bindings: continuity: provider: "agnir" - compatibility: "0.1" - profile: "repository-filesystem/0.1" + compatibility: "0.2" + profile: "repository-filesystem/0.2" config: discovery: "AGNIR.yaml" activation: "AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml" + lineage: "urn:svif:lineage:authoritative" + vcs_selector: "refs/heads/main" execution: [] capabilities: [] @@ -42,6 +44,7 @@ checks: portable_contracts: "conformance/check_contracts.py" runtime_kernel: "tests/test_runtime.py" agnir_continuity: "tests/test_agnir_continuity.py" + agnir_stable_migration: "tests/test_agnir_stable_migration.py" chatgpt_surface: "tests/test_chatgpt_surface.py" cloudflare_capability: "tests/test_cloudflare_capability.py" founding_e2e: "tests/test_founding_e2e.py" diff --git a/checks/check_repository.py b/checks/check_repository.py old mode 100644 new mode 100755 index dc512e4..931f5bd --- a/checks/check_repository.py +++ b/checks/check_repository.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from __future__ import annotations +import re import sys from pathlib import Path @@ -18,6 +19,29 @@ def require_text(text: str, needles: list[str], label: str) -> None: fail(f"{label} missing required product-architecture marker: {needle}") +def parse_scalar_paths(text: str) -> dict[tuple[str, ...], str | None]: + """Parse the small nested scalar YAML subset used by Svif/Agnir bindings.""" + values: dict[tuple[str, ...], str | None] = {} + stack: list[tuple[int, str]] = [] + for raw in text.splitlines(): + if not raw.strip() or raw.lstrip().startswith("#") or raw.lstrip().startswith("-"): + continue + indent = len(raw) - len(raw.lstrip(" ")) + match = re.match(r"^\s*([A-Za-z0-9_./-]+):\s*(.*?)\s*$", raw) + if not match: + continue + key, scalar = match.groups() + while stack and indent <= stack[-1][0]: + stack.pop() + if scalar == "": + stack.append((indent, key)) + continue + if len(scalar) >= 2 and scalar[0] == scalar[-1] and scalar[0] in {'"', "'"}: + scalar = scalar[1:-1] + values[tuple([item[1] for item in stack] + [key])] = None if scalar in {"null", "~"} else scalar + return values + + def require_readme_entry_guide( path: str, *, @@ -85,8 +109,6 @@ def require_readme_diagrams( for marker in runtime_forbidden_markers: if marker in runtime_text: fail(f"{path} Runtime / Operation Flow must not include installation mutation marker: {marker}") - - # Keep README Mermaid syntax deliberately conservative for GitHub rendering. diagram_text = architecture_text + runtime_text for risky in ("<-->", ".-> T", "\\n"): if risky in diagram_text: @@ -173,37 +195,81 @@ def require_full_repository_tree() -> None: def require_agnir_activation() -> None: agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") - agnir = (ROOT / "AGNIR.yaml").read_text(encoding="utf-8") - svif = (ROOT / "SVIF.yaml").read_text(encoding="utf-8") + agnir_text = (ROOT / "AGNIR.yaml").read_text(encoding="utf-8") + svif_text = (ROOT / "SVIF.yaml").read_text(encoding="utf-8") + agnir = parse_scalar_paths(agnir_text) + svif = parse_scalar_paths(svif_text) require_text(agents, ["Agnir Project Instructions", "README.md", "AGNIR.yaml"], "AGENTS.md") if ".agnir/state.md" in agents or ".agnir/next-actions.md" in agents: fail("AGENTS.md must remain a locator and must not duplicate durable Project memory") - require_text(readme, [ - "## Agnir Project Instructions", - "authorized Project Entry Point", + require_text( + readme, + [ + "## Agnir Project Instructions", + "authorized Project Entry Point", + "AGNIR.yaml", + "Current State", + "Next Actions", + "Decisions", + "Evidence", + "Project root -> AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml -> declared durable memory", + ], + "README.md Agnir activation", + ) + + project_identity = agnir.get(("project", "identity")) + svif_project_identity = svif.get(("project", "identity")) + if not project_identity or project_identity != svif_project_identity: + fail("AGNIR.yaml and SVIF.yaml must identify the same non-empty Project") + + version = agnir.get(("agnir", "version")) + profile = agnir.get(("agnir", "discovery_profile")) + compatibility = svif.get(("bindings", "continuity", "compatibility")) + bound_profile = svif.get(("bindings", "continuity", "profile")) + provider = svif.get(("bindings", "continuity", "provider")) + + if provider != "agnir": + fail("Svif founding continuity binding must identify provider agnir") + supported = { + "0.1": "repository-filesystem/0.1", + "0.2": "repository-filesystem/0.2", + } + if version not in supported: + fail(f"Svif repository selected unsupported Agnir Core compatibility: {version!r}") + if profile != supported[version]: + fail(f"Agnir Core {version} must use profile {supported[version]}, got {profile!r}") + if compatibility != version or bound_profile != profile: + fail("SVIF.yaml Continuity Provider compatibility/profile must match AGNIR.yaml") + + require_text( + agnir_text, + [ + 'state: ".agnir/state.md"', + 'next_actions: ".agnir/next-actions.md"', + 'decisions: ".agnir/decisions.md"', + 'evidence: ".agnir/evidence/"', + ], "AGNIR.yaml", - "Current State", - "Next Actions", - "Decisions", - "Evidence", - "Project root -> AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml -> declared durable memory", - ], "README.md Agnir activation") - - require_text(agnir, [ - 'version: "0.1"', - 'discovery_profile: "repository-filesystem/0.1"', - 'state: ".agnir/state.md"', - 'next_actions: ".agnir/next-actions.md"', - 'decisions: ".agnir/decisions.md"', - 'evidence: ".agnir/evidence/"', - ], "AGNIR.yaml") - require_text(svif, [ - 'compatibility: "0.1"', - 'profile: "repository-filesystem/0.1"', - 'activation: "AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml"', - ], "SVIF.yaml Agnir binding") + ) + require_text( + svif_text, + ['activation: "AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml"'], + "SVIF.yaml Agnir binding", + ) + + if version == "0.2": + lineage = agnir.get(("continuity", "lineage")) + bound_lineage = svif.get(("bindings", "continuity", "config", "lineage")) + selector = agnir.get(("extensions", "agnir/vcs", "lineage_binding", "selector")) + bound_selector = svif.get(("bindings", "continuity", "config", "vcs_selector")) + if not lineage or bound_lineage != lineage: + fail("Core 0.2 requires one logical lineage and matching Svif provider binding") + if not selector or bound_selector != selector: + fail("Core 0.2 VCS validation requires matching durable selector binding") + if selector == lineage: + fail("VCS selector must remain distinct from logical lineage identity") for path in (".agnir/state.md", ".agnir/next-actions.md", ".agnir/decisions.md", ".agnir/evidence"): if not (ROOT / path).exists(): @@ -252,10 +318,8 @@ def main() -> None: upgrade_prompt="Upgrade the Agnir used by this Project to the latest stable release: https://github.com/iorLab/agnir", normal_use_marker="No recurring Svif installation prompt is required.", surface_markers=( - "[EDIT: add entry only]", - "[ADD] founding Agnir discovery anchor", - "[ADD] Project-owned durable continuity", - "[ADD] Svif Project Binding", + "[EDIT: add entry only]", "[ADD] founding Agnir discovery anchor", + "[ADD] Project-owned durable continuity", "[ADD] Svif Project Binding", "intentionally bound to another Continuity Provider", ), ) @@ -268,10 +332,8 @@ def main() -> None: upgrade_prompt="把这个 Project 使用的 Agnir 升级到最新稳定版:https://github.com/iorLab/agnir", normal_use_marker="不需要在每次对话里重复 Svif 安装提示。", surface_markers=( - "[编辑:仅添加入口]", - "[新增] founding Agnir discovery anchor", - "[新增] Project 自己拥有的 durable continuity", - "[新增] Svif Project Binding", + "[编辑:仅添加入口]", "[新增] founding Agnir discovery anchor", + "[新增] Project 自己拥有的 durable continuity", "[新增] Svif Project Binding", "明确绑定其他 Continuity Provider", ), ) @@ -279,15 +341,9 @@ def main() -> None: "README.md", ("## Architecture Diagram", "## Runtime / Operation Flow"), architecture_markers=( - "non-destructive first-use setup", - "EDIT: add activation locator only", - "EDIT: add Agnir instructions only", - "ADD: founding continuity", - "ADD: Project binding", - "Svif Orchestrator", - "Continuity Provider", - "Execution integration", - "Capability Providers", + "non-destructive first-use setup", "EDIT: add activation locator only", + "EDIT: add Agnir instructions only", "ADD: founding continuity", "ADD: Project binding", + "Svif Orchestrator", "Continuity Provider", "Execution integration", "Capability Providers", ), runtime_forbidden_markers=("EDIT: add", "ADD: founding", "ADD: Project binding"), ) @@ -295,15 +351,9 @@ def main() -> None: "README.zh-CN.md", ("## 架构图", "## 运行流程"), architecture_markers=( - "非破坏性 first-use setup", - "编辑:仅添加 activation locator", - "编辑:仅添加 Agnir instructions", - "新增:founding continuity", - "新增:Project binding", - "Svif 编排器", - "项目连续性提供者", - "执行环境适配层", - "能力提供层", + "非破坏性 first-use setup", "编辑:仅添加 activation locator", + "编辑:仅添加 Agnir instructions", "新增:founding continuity", "新增:Project binding", + "Svif 编排器", "项目连续性提供者", "执行环境适配层", "能力提供层", ), runtime_forbidden_markers=("编辑:仅添加", "新增:founding", "新增:Project binding"), ) @@ -341,8 +391,8 @@ def main() -> None: cloudflare = (ROOT / "src/svif/capabilities/cloudflare.py").read_text(encoding="utf-8") require_text(cloudflare, [ - 'provider_id = "cloudflare.workers"', "class CloudflareWorkersTransport", "class CloudflareWorkersCapabilityProvider", - "def actuate(", "def observe(", + 'provider_id = "cloudflare.workers"', "class CloudflareWorkersTransport", + "class CloudflareWorkersCapabilityProvider", "def actuate(", "def observe(", ], "src/svif/capabilities/cloudflare.py") plugin = (ROOT / "plugin/skills/svif/SKILL.md").read_text(encoding="utf-8") @@ -366,7 +416,7 @@ def main() -> None: "README.zh-CN.md", "Plugin MVP", ], "Agnir state") - print("PASS: Svif product repository integrity, Agnir activation, Plugin packaging, and single-repository architecture baseline") + print("PASS: Svif product repository integrity, coherent Agnir binding, Plugin packaging, and single-repository architecture baseline") if __name__ == "__main__": diff --git a/plugin/skills/svif/SKILL.md b/plugin/skills/svif/SKILL.md index 1591e7a..39cbf4f 100644 --- a/plugin/skills/svif/SKILL.md +++ b/plugin/skills/svif/SKILL.md @@ -26,15 +26,17 @@ For the current repository/filesystem founding path, bootstrap the Project in th 5. Run fresh activation from the selected Project root only: `Project root -> AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml -> declared durable memory`. Validate the Agnir identity/profile/version and then validate that `SVIF.yaml` identifies the same Project and continuity binding. 6. Once bootstrap passes, continue the user's original Project task in the same operation. Do not make the user issue a separate Agnir initialization prompt and do not stop merely because the Project started without Agnir. +The bootstrap above is the immutable onboarding contract of the released `v0.2.0-preview.1` distribution. Migrating the Svif repository Project itself to a newer Agnir compatibility line does not retroactively rewrite that released bootstrap baseline. A future Svif distribution may deliberately move first-use bootstrap to Core/profile `0.2`, but that requires its own versioned distribution change and evidence. + This bootstrap behavior consumes Agnir protocol/profile semantics through Svif's founding Continuity Provider integration; it MUST NOT require the Agnir Skill repository, a previous Agnir installation conversation, GitHub, or another execution surface as a runtime prerequisite. A compatible surface may delegate to an available Agnir installer, but successful first use must remain possible from the Svif Plugin procedure itself. Do not treat partial or contradictory Agnir/Svif artifacts as a clean first-use bootstrap. If any durable surface shows that the Project already intends to use Agnir but activation/discovery is incomplete or broken, enter repair and preserve the applicable Agnir failure class. If `SVIF.yaml` or another durable binding intentionally selects a different Continuity Provider, do not overwrite it with Agnir; use the configured provider when supported or surface a binding/support blocker. If the current execution surface cannot perform the required non-destructive Project writes, report that bootstrap capability blocker rather than pretending that pre-initialization was a user prerequisite. -For an Agent-operable Agnir Project using `repository-filesystem/0.1`, the durable activation route is mandatory before normal Project work: +For an Agent-operable Agnir Project using a supported repository/filesystem profile, the durable activation route is mandatory before normal Project work: `Project root -> AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml -> declared durable memory` -Agnir Agent activation and Core discovery are distinct layers. When this Skill is operating an Agent-operable Project under `repository-filesystem/0.1`, treat the durable `AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml` route as part of the Project activation contract, not as an optional convenience. Validate that the route is present, points to the canonical README Agnir section, and contains no unresolved material instruction conflict before treating Agent activation as healthy. The fact that the current Agent can directly open `AGNIR.yaml` MUST NOT be used to bypass a missing, stale, contradictory, or predecessor-private activation route or to claim that a fresh Agent can resume from the Project root. +Agnir Agent activation and Core discovery are distinct layers. When this Skill is operating an Agent-operable Project under a supported repository/filesystem profile, treat the durable `AGENTS.md -> README.md / Agnir Project Instructions -> AGNIR.yaml` route as part of the Project activation contract, not as an optional convenience. Validate that the route is present, points to the canonical README Agnir section, and contains no unresolved material instruction conflict before treating Agent activation as healthy. The fact that the current Agent can directly open `AGNIR.yaml` MUST NOT be used to bypass a missing, stale, contradictory, or predecessor-private activation route or to claim that a fresh Agent can resume from the Project root. Do not validate activation by heading/link presence alone. The canonical README `## Agnir Project Instructions` section itself MUST satisfy the current profile activation contract: it must state that the Project uses Agnir for durable continuity and, before Project work, direct a fresh Agent to treat the Project root as the authorized Project Entry Point, read top-level `AGNIR.yaml`, load Current State and Next Actions, load Decisions and Evidence when relevant, prefer durable Agnir Project truth over chat/private Agent memory unless superseded by newer Principal instruction or directly observed current Project fact, and checkpoint material continuity changes at an intentional save/finish boundary. If any required instruction is missing, materially weakened, or contradicted, activation is not healthy even when `AGENTS.md` reaches the correct heading; repair that earliest activation-contract defect when authorized and rerun activation from the Project root. @@ -53,19 +55,20 @@ When `AGNIR.yaml` is available, read it before substantive work. Before loading 1. validate `agnir.version` against the Agnir Core compatibility supported by the current Project binding; 2. validate `agnir.discovery_profile` against the already selected discovery profile; 3. verify that `project.identity` matches the Project selected by the authorized Project Entry Point or trusted binding context; -4. resolve the required memory locators only after those compatibility and identity checks pass. +4. for Core `0.2`, require a non-empty logical `continuity.lineage`; when a VCS selector is selected by trusted context, require the durable selector binding and verify that it matches that context while remaining distinct from the logical lineage identity; +5. resolve the required memory locators only after those compatibility, identity, lineage, and applicable selector-binding checks pass. -For the `repository-filesystem/0.1` profile, relative memory locators remain scoped to the selected Project root after resolving filesystem indirection. A relative locator that traverses a symlink or other indirection outside that root MUST NOT become an implicitly authorized external Locator Chain merely because the target is readable. Follow external memory only through an explicit durable authorized binding/Locator Chain; otherwise preserve the applicable discovery failure, including `AGNIR_DISCOVERY_UNAUTHORIZED` when the locator is known but authorization is absent or denied. +For supported `repository-filesystem/0.1` and `repository-filesystem/0.2` profiles, relative memory locators remain scoped to the selected Project root after resolving filesystem indirection. A relative locator that traverses a symlink or other indirection outside that root MUST NOT become an implicitly authorized external Locator Chain merely because the target is readable. Follow external memory only through an explicit durable authorized binding/Locator Chain; otherwise preserve the applicable discovery failure, including `AGNIR_DISCOVERY_UNAUTHORIZED` when the locator is known but authorization is absent or denied. A Locator Chain hop may use an environment binding only when that binding is stable and durably associated with the selected Project. A value that exists only in the current process environment, temporary workspace metadata, a prior conversation, private model memory, or a prompt-provided secret MUST NOT become continuity authority merely because it makes the locator resolve in this run. Require the Project Entry Point or another durable Project-owned binding to establish how a fresh Executor can recover the same locator and invoke any required authorization without predecessor-private context. If that durable association cannot be established, surface the applicable discovery failure rather than accepting an ephemeral successful resolution or checkpointing it as resumable continuity. For repository-aware Projects that declare `extensions.agnir/repository.canonical` and `authoritative_ref`, treat those values as durable backend metadata for canonical continuity, not as decorative provenance. Before a state-dependent write or checkpoint, determine whether the selected working copy/revision is actually on the declared canonical repository/ref. A detached commit, pull-request checkout, temporary branch, fork, mirror, or otherwise non-authoritative execution copy MAY be used for observation, implementation, and verification, but MUST NOT silently become the canonical continuity write target merely because it is writable. Reconcile accepted changes back to the declared authoritative ref, or surface the repository/ref mismatch and leave the canonical checkpoint unchanged until the Project policy or trusted Principal explicitly authorizes a different durable binding. Package revision identity and target-Project authoritative-ref identity are separate facts and MUST NOT be conflated. -For the current Svif repository binding, the expected values are Agnir Core `0.1`, profile `repository-filesystem/0.1`, and Project identity `urn:svif:project:svif-core`. Treat these as Project-binding facts, not universal Agnir constants. +For the current Svif repository binding, the expected values are Agnir Core `0.2`, profile `repository-filesystem/0.2`, Project identity `urn:svif:project:svif-core`, one explicit logical Continuity Lineage, and a matching durable VCS selector binding. Treat these as Project-binding facts, not universal Agnir constants. The released `v0.2.0-preview.1` first-use bootstrap remains on its separately versioned Core/profile `0.1` baseline. -Do not load state and then retroactively decide whether it belonged to the selected Project. Unsupported Core/profile compatibility must surface an explicit discovery failure such as `AGNIR_DISCOVERY_UNSUPPORTED_VERSION`; a selected-root identity mismatch must surface `AGNIR_DISCOVERY_PROJECT_MISMATCH`. A known required locator whose authorization is absent or denied must remain `AGNIR_DISCOVERY_UNAUTHORIZED` when that distinction can safely be made; a required locator that cannot resolve to durable state must remain `AGNIR_DISCOVERY_UNRESOLVABLE`. A Locator Chain that loops rather than terminating in required durable state must remain `AGNIR_DISCOVERY_CYCLE`; state known to be superseded or non-authoritative must remain `AGNIR_DISCOVERY_STALE`; and material contradiction between the Discovery Record and resolved memory, or within the resolved memory itself, must remain `AGNIR_DISCOVERY_INCONSISTENT` until safe continuation is re-established. None of these failures grants permission to search sibling repositories, parent/child Projects, home directories, chat history, or retired layouts for substitute state. +Do not load state and then retroactively decide whether it belonged to the selected Project. Unsupported Core/profile compatibility must surface an explicit discovery failure such as `AGNIR_DISCOVERY_UNSUPPORTED_VERSION`; a selected-root identity mismatch must surface `AGNIR_DISCOVERY_PROJECT_MISMATCH`. Core `0.2` without a selected logical lineage must surface `AGNIR_LINEAGE_REQUIRED`. A selected VCS context without a durable selector binding, or with a conflicting selector binding, must preserve the applicable binding failure rather than guessing another lineage. A known required locator whose authorization is absent or denied must remain `AGNIR_DISCOVERY_UNAUTHORIZED` when that distinction can safely be made; a required locator that cannot resolve to durable state must remain `AGNIR_DISCOVERY_UNRESOLVABLE`. A Locator Chain that loops rather than terminating in required durable state must remain `AGNIR_DISCOVERY_CYCLE`; state known to be superseded or non-authoritative must remain `AGNIR_DISCOVERY_STALE`; and material contradiction between the Discovery Record and resolved memory, or within the resolved memory itself, must remain `AGNIR_DISCOVERY_INCONSISTENT` until safe continuation is re-established. None of these failures grants permission to search sibling repositories, parent/child Projects, home directories, chat history, or retired layouts for substitute state. -After validation, treat the Project-managed Agnir state as the durable continuity authority for current state, next actions, decisions, and referenced evidence, but reconcile conflicting truth using Agnir Core `0.1` precedence unless stricter Project policy applies: directly observed current Project or relevant external-system state first; explicit current Principal instruction or policy second; current durable Agnir state third; older checkpoint/evidence fourth; Executor-private context last. Material unresolved uncertainty must be surfaced rather than guessed. A newer observed fact or Principal instruction that supersedes durable state must be reconciled back into the Project-owned checkpoint instead of remaining only in transient execution context. +After validation, treat the Project-managed Agnir state as the durable continuity authority for current state, next actions, decisions, and referenced evidence, but reconcile conflicting truth using the precedence defined by the selected compatible Agnir Core line unless stricter Project policy applies: directly observed current Project or relevant external-system state first; explicit current Principal instruction or policy second; current durable Agnir state third; older checkpoint/evidence fourth; Executor-private context last. Material unresolved uncertainty must be surfaced rather than guessed. A newer observed fact or Principal instruction that supersedes durable state must be reconciled back into the Project-owned checkpoint instead of remaining only in transient execution context. This truth-reconciliation precedence does not grant protected execution authority. Principal approval or policy is usable for protected effects only when it arrives through the applicable trusted integration boundary, and directly observed state does not replace exact-subject verification, required authority, or independent post-effect observation. @@ -81,7 +84,7 @@ If more than one Project is involved, keep each Project's durable state isolated Load current state and next actions first. Then read only decisions and evidence that materially constrain the requested operation. Avoid pulling historical or retired artifacts back into active architecture unless the current Project explicitly declares them authoritative. -Use the Project's canonical repository or substrate when one is declared. For `iorLab/svif`, `main` is the active line and repository-managed Agnir state is canonical. Svif currently consumes Agnir Core compatibility `0.1` through the `repository-filesystem/0.1` profile; Agnir repository release `0.1.1` is a separate SemVer layer and must not be confused with the Core/profile compatibility identifiers. +Use the Project's canonical repository or substrate when one is declared. For `iorLab/svif`, `main` is the active authoritative line and repository-managed Agnir state is canonical. The Svif repository Project now consumes published Agnir repository release `v0.2.0`, Core compatibility `0.2`, and profile `repository-filesystem/0.2`; its logical lineage and VCS selector binding are declared separately in `AGNIR.yaml` / `SVIF.yaml`. The released `v0.2.0-preview.1` distribution still bootstraps new Projects on its published Core/profile `0.1` baseline until a later Svif distribution release deliberately changes that behavior. Repository SemVer and Core/profile compatibility identifiers are separate version layers and must not be conflated. ## 3. Execute through the Svif lifecycle diff --git a/src/svif/continuity/agnir.py b/src/svif/continuity/agnir.py index 4a6e38a..8c5f722 100644 --- a/src/svif/continuity/agnir.py +++ b/src/svif/continuity/agnir.py @@ -4,7 +4,7 @@ import json import os import re -from dataclasses import asdict +from dataclasses import asdict, dataclass from pathlib import Path from svif.runtime import BindingError, ContinuitySnapshot, OperationOutcome @@ -18,20 +18,45 @@ def __init__(self, code: str, message: str) -> None: self.code = code +@dataclass(frozen=True) +class _ResolvedAgnir: + version: str + profile: str + lineage_identity: str | None + vcs_selector: str | None + state: Path + next_actions: Path + decisions: Path | None + evidence: Path | None + + class AgnirFilesystemContinuityProvider: - """Agnir `repository-filesystem/0.1` Continuity Provider for Svif. + """Agnir repository/filesystem Continuity Provider for Svif. - The Project root is the authorized Project Entry Point. This adapter - intentionally implements only Agnir's current repository/filesystem profile; - that profile is not part of the generic Svif Orchestrator contract. + The adapter supports the published Core/profile `0.1` line and the + experimental Core/profile `0.2` lineage-aware line. Provider-specific + lineage and selector semantics stay inside this adapter; the Svif + Orchestrator remains Continuity-Provider-neutral. """ provider_id = "agnir" - _CORE_VERSION = "0.1" - _PROFILE = "repository-filesystem/0.1" + _SUPPORTED_PROFILES = { + "0.1": "repository-filesystem/0.1", + "0.2": "repository-filesystem/0.2", + } - def __init__(self, project_root: str | Path) -> None: + def __init__( + self, + project_root: str | Path, + *, + expected_core_version: str | None = None, + expected_profile: str | None = None, + selected_vcs_selector: str | None = None, + ) -> None: self.project_root = Path(project_root).resolve() + self.expected_core_version = expected_core_version + self.expected_profile = expected_profile + self.selected_vcs_selector = selected_vcs_selector @staticmethod def _strip_scalar(value: str) -> str | None: @@ -43,28 +68,33 @@ def _strip_scalar(value: str) -> str | None: return value @classmethod - def _parse_discovery(cls, text: str) -> dict[tuple[str, str], str | None]: - """Parse only the scalar subset required by repository-filesystem/0.1. + def _parse_discovery(cls, text: str) -> dict[tuple[str, ...], str | None]: + """Parse the scalar YAML subset used by the repository profiles. - This is deliberately not a general YAML parser. Unsupported YAML forms - fail later as missing/inconsistent required semantics rather than being - guessed by the adapter. + This is deliberately not a general YAML parser. It recognizes nested + mapping/scalar paths by indentation and ignores list items/complex YAML. + Unsupported forms fail later as missing or inconsistent semantics. """ - values: dict[tuple[str, str], str | None] = {} - section: str | None = None + values: dict[tuple[str, ...], str | None] = {} + stack: list[tuple[int, str]] = [] for raw in text.splitlines(): if not raw.strip() or raw.lstrip().startswith("#"): continue - top = re.match(r"^([A-Za-z0-9_/-]+):\s*$", raw) - if top: - section = top.group(1) + if raw.lstrip().startswith("-"): + continue + indent = len(raw) - len(raw.lstrip(" ")) + match = re.match(r"^\s*([A-Za-z0-9_./-]+):\s*(.*?)\s*$", raw) + if not match: continue - if section is None: + key, scalar_text = match.groups() + while stack and indent <= stack[-1][0]: + stack.pop() + if scalar_text == "": + stack.append((indent, key)) continue - scalar = re.match(r"^\s{2}([A-Za-z0-9_/-]+):\s*(.*?)\s*$", raw) - if scalar: - values[(section, scalar.group(1))] = cls._strip_scalar(scalar.group(2)) + path = tuple([item[1] for item in stack] + [key]) + values[path] = cls._strip_scalar(scalar_text) return values @staticmethod @@ -99,7 +129,7 @@ def _resolve_locator( ) return candidate - def _discover(self, project_identity: str) -> dict[str, Path | None]: + def _discover(self, project_identity: str) -> _ResolvedAgnir: discovery = self.project_root / "AGNIR.yaml" if not discovery.is_file(): raise self._fail( @@ -108,16 +138,29 @@ def _discover(self, project_identity: str) -> dict[str, Path | None]: ) values = self._parse_discovery(discovery.read_text(encoding="utf-8")) + version = values.get(("agnir", "version")) + profile = values.get(("agnir", "discovery_profile")) - if values.get(("agnir", "version")) != self._CORE_VERSION: + if not isinstance(version, str) or version not in self._SUPPORTED_PROFILES: + raise self._fail( + "AGNIR_DISCOVERY_UNSUPPORTED_VERSION", + f"unsupported Agnir Core version: {version!r}", + ) + expected_profile_for_version = self._SUPPORTED_PROFILES[version] + if profile != expected_profile_for_version: + raise self._fail( + "AGNIR_DISCOVERY_INCONSISTENT", + f"Core {version} requires discovery profile {expected_profile_for_version!r}, discovered {profile!r}", + ) + if self.expected_core_version is not None and version != self.expected_core_version: raise self._fail( "AGNIR_DISCOVERY_UNSUPPORTED_VERSION", - f"expected Agnir Core {self._CORE_VERSION}", + f"Svif binding expects Agnir Core {self.expected_core_version}, discovered {version}", ) - if values.get(("agnir", "discovery_profile")) != self._PROFILE: + if self.expected_profile is not None and profile != self.expected_profile: raise self._fail( "AGNIR_DISCOVERY_INCONSISTENT", - f"expected discovery profile {self._PROFILE}", + f"Svif binding expects discovery profile {self.expected_profile!r}, discovered {profile!r}", ) discovered_identity = values.get(("project", "identity")) @@ -127,6 +170,31 @@ def _discover(self, project_identity: str) -> dict[str, Path | None]: f"expected {project_identity!r}, discovered {discovered_identity!r}", ) + lineage_identity = values.get(("continuity", "lineage")) + if version == "0.2": + if not isinstance(lineage_identity, str) or not lineage_identity: + raise self._fail( + "AGNIR_LINEAGE_REQUIRED", + "Core 0.2 repository/filesystem discovery requires continuity.lineage", + ) + else: + lineage_identity = None + + binding_selector = values.get( + ("extensions", "agnir/vcs", "lineage_binding", "selector") + ) + if self.selected_vcs_selector is not None and version == "0.2": + if not isinstance(binding_selector, str) or not binding_selector: + raise self._fail( + "AGNIR_VCS_LINEAGE_BINDING_REQUIRED", + "selected VCS context has no durable lineage selector binding", + ) + if binding_selector != self.selected_vcs_selector: + raise self._fail( + "AGNIR_VCS_LINEAGE_BINDING_MISMATCH", + f"selected VCS selector {self.selected_vcs_selector!r} conflicts with durable binding {binding_selector!r}", + ) + paths = { "state": self._resolve_locator( values.get(("memory", "state")), required=True, kind="Current State" @@ -157,7 +225,16 @@ def _discover(self, project_identity: str) -> dict[str, Path | None]: "Evidence locator is not a directory", ) - return paths + return _ResolvedAgnir( + version=version, + profile=profile, + lineage_identity=lineage_identity, + vcs_selector=binding_selector if isinstance(binding_selector, str) else None, + state=paths["state"], + next_actions=paths["next_actions"], + decisions=paths["decisions"], + evidence=paths["evidence"], + ) @staticmethod def _read_optional(path: Path | None) -> str | None: @@ -173,14 +250,18 @@ def _read_evidence(path: Path | None) -> dict[str, str]: if item.is_file() } + def resolve_lineage(self, project_identity: str) -> str | None: + """Return the selected logical Agnir lineage, if the compatibility line has one.""" + return self._discover(project_identity).lineage_identity + def load(self, project_identity: str) -> ContinuitySnapshot: - paths = self._discover(project_identity) + resolved = self._discover(project_identity) return ContinuitySnapshot( project_identity=project_identity, - state=self._read_optional(paths["state"]), - next_actions=self._read_optional(paths["next_actions"]), - decisions=self._read_optional(paths["decisions"]), - evidence=self._read_evidence(paths["evidence"]), + state=self._read_optional(resolved.state), + next_actions=self._read_optional(resolved.next_actions), + decisions=self._read_optional(resolved.decisions), + evidence=self._read_evidence(resolved.evidence), ) @staticmethod @@ -198,7 +279,7 @@ def _atomic_write(path: Path, content: str) -> None: os.replace(tmp, path) def checkpoint(self, outcome: OperationOutcome) -> None: - paths = self._discover(outcome.project_identity) + resolved = self._discover(outcome.project_identity) update = outcome.continuity_update state = self._require_text_update(update.state, "Current State") @@ -206,26 +287,29 @@ def checkpoint(self, outcome: OperationOutcome) -> None: decisions = self._require_text_update(update.decisions, "Decisions") if state is not None: - self._atomic_write(paths["state"], state) + self._atomic_write(resolved.state, state) if next_actions is not None: - self._atomic_write(paths["next_actions"], next_actions) + self._atomic_write(resolved.next_actions, next_actions) if decisions is not None: - if paths["decisions"] is None: + if resolved.decisions is None: raise self._fail( "AGNIR_DISCOVERY_UNRESOLVABLE", "cannot persist Decisions because the Discovery Record has no Decisions locator", ) - self._atomic_write(paths["decisions"], decisions) + self._atomic_write(resolved.decisions, decisions) - evidence_dir = paths["evidence"] - if evidence_dir is not None: + if resolved.evidence is not None: digest = hashlib.sha256( - f"{outcome.project_identity}\0{outcome.operation_id}".encode("utf-8") + ( + f"{outcome.project_identity}\0{resolved.lineage_identity or ''}\0" + f"{outcome.operation_id}" + ).encode("utf-8") ).hexdigest()[:16] - evidence_path = evidence_dir / f"svif-operation-{digest}.json" + evidence_path = resolved.evidence / f"svif-operation-{digest}.json" payload = { "svif_runtime_checkpoint": "0.1", "project_identity": outcome.project_identity, + "agnir_lineage": resolved.lineage_identity, "operation_id": outcome.operation_id, "subject_identity": outcome.subject_identity, "externally_effectful": outcome.externally_effectful, @@ -236,5 +320,6 @@ def checkpoint(self, outcome: OperationOutcome) -> None: json.dumps(payload, indent=2, sort_keys=True) + "\n", ) - # Do not claim resumability until the resulting locator chain resolves. + # Do not claim resumability until the resulting locator chain, Project + # identity, logical lineage, and optional VCS selector binding resolve. self._discover(outcome.project_identity) diff --git a/tests/test_agnir_continuity.py b/tests/test_agnir_continuity.py index 13377e9..70332a9 100644 --- a/tests/test_agnir_continuity.py +++ b/tests/test_agnir_continuity.py @@ -25,25 +25,52 @@ def write_project( root: Path, *, version: str = "0.1", + profile: str | None = None, identity: str = PROJECT, state_locator: str = ".agnir/state.md", + lineage: str | None = None, + vcs_selector: str | None = None, ) -> None: + if profile is None: + profile = f"repository-filesystem/{version}" if version in {"0.1", "0.2"} else "repository-filesystem/0.1" (root / ".agnir/evidence").mkdir(parents=True) (root / ".agnir/state.md").write_text("# State\nold\n", encoding="utf-8") (root / ".agnir/next-actions.md").write_text("# Next\nold\n", encoding="utf-8") (root / ".agnir/decisions.md").write_text("# Decisions\nold\n", encoding="utf-8") (root / ".agnir/evidence/seed.md").write_text("# Seed\n", encoding="utf-8") + + continuity = "" + if version == "0.2" or lineage is not None: + continuity = ( + "\ncontinuity:\n" + f' lineage: "{lineage or "urn:test:lineage:default"}"\n' + ) + + extensions = "" + if vcs_selector is not None: + extensions = ( + "\nextensions:\n" + " agnir/vcs:\n" + ' branch_continuity: "lineage-bound"\n' + ' integration_reconciliation: "required"\n' + " lineage_binding:\n" + ' kind: "vcs-ref"\n' + f' selector: "{vcs_selector}"\n' + ) + (root / "AGNIR.yaml").write_text( "agnir:\n" f' version: "{version}"\n' - ' discovery_profile: "repository-filesystem/0.1"\n\n' + f' discovery_profile: "{profile}"\n\n' "project:\n" - f' identity: "{identity}"\n\n' - "memory:\n" + f' identity: "{identity}"\n' + + continuity + + "\nmemory:\n" f' state: "{state_locator}"\n' ' next_actions: ".agnir/next-actions.md"\n' ' decisions: ".agnir/decisions.md"\n' - ' evidence: ".agnir/evidence/"\n', + ' evidence: ".agnir/evidence/"\n' + + extensions, encoding="utf-8", ) @@ -65,7 +92,7 @@ def execute(self, context, request) -> WorkResult: class AgnirFilesystemContinuityTests(unittest.TestCase): - def test_loads_repository_filesystem_profile(self) -> None: + def test_loads_repository_filesystem_profile_0_1(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) write_project(root) @@ -74,11 +101,32 @@ def test_loads_repository_filesystem_profile(self) -> None: snapshot = provider.load(PROJECT) self.assertEqual(snapshot.project_identity, PROJECT) + self.assertIsNone(provider.resolve_lineage(PROJECT)) self.assertIn("old", snapshot.state) self.assertIn("old", snapshot.next_actions) self.assertIn("old", snapshot.decisions) self.assertIn("seed.md", snapshot.evidence) + def test_loads_repository_filesystem_profile_0_2_and_resolves_logical_lineage(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + selector = "refs/heads/feature/parallel" + lineage = "urn:test:lineage:parallel" + write_project(root, version="0.2", lineage=lineage, vcs_selector=selector) + provider = AgnirFilesystemContinuityProvider( + root, + expected_core_version="0.2", + expected_profile="repository-filesystem/0.2", + selected_vcs_selector=selector, + ) + + snapshot = provider.load(PROJECT) + + self.assertEqual(snapshot.project_identity, PROJECT) + self.assertEqual(provider.resolve_lineage(PROJECT), lineage) + self.assertIn("old", snapshot.state) + self.assertIn("seed.md", snapshot.evidence) + def test_project_mismatch_preserves_agnir_failure_class(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -101,6 +149,68 @@ def test_unsupported_version_preserves_agnir_failure_class(self) -> None: self.assertEqual(raised.exception.code, "AGNIR_DISCOVERY_UNSUPPORTED_VERSION") + def test_svif_binding_can_require_core_0_2(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_project(root, version="0.1") + provider = AgnirFilesystemContinuityProvider( + root, + expected_core_version="0.2", + expected_profile="repository-filesystem/0.2", + ) + + with self.assertRaises(AgnirDiscoveryError) as raised: + provider.load(PROJECT) + + self.assertEqual(raised.exception.code, "AGNIR_DISCOVERY_UNSUPPORTED_VERSION") + + def test_core_0_2_requires_logical_lineage(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_project(root, version="0.2", lineage="urn:test:lineage:temporary") + text = (root / "AGNIR.yaml").read_text(encoding="utf-8") + text = text.replace('\ncontinuity:\n lineage: "urn:test:lineage:temporary"\n', "") + (root / "AGNIR.yaml").write_text(text, encoding="utf-8") + provider = AgnirFilesystemContinuityProvider(root) + + with self.assertRaises(AgnirDiscoveryError) as raised: + provider.load(PROJECT) + + self.assertEqual(raised.exception.code, "AGNIR_LINEAGE_REQUIRED") + + def test_selected_vcs_selector_must_match_durable_binding(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_project( + root, + version="0.2", + lineage="urn:test:lineage:parallel", + vcs_selector="refs/heads/feature/a", + ) + provider = AgnirFilesystemContinuityProvider( + root, + selected_vcs_selector="refs/heads/feature/b", + ) + + with self.assertRaises(AgnirDiscoveryError) as raised: + provider.load(PROJECT) + + self.assertEqual(raised.exception.code, "AGNIR_VCS_LINEAGE_BINDING_MISMATCH") + + def test_selected_vcs_selector_requires_durable_binding(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_project(root, version="0.2", lineage="urn:test:lineage:parallel") + provider = AgnirFilesystemContinuityProvider( + root, + selected_vcs_selector="refs/heads/feature/a", + ) + + with self.assertRaises(AgnirDiscoveryError) as raised: + provider.load(PROJECT) + + self.assertEqual(raised.exception.code, "AGNIR_VCS_LINEAGE_BINDING_REQUIRED") + def test_locator_cannot_escape_project_root(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -113,7 +223,7 @@ def test_locator_cannot_escape_project_root(self) -> None: self.assertEqual(raised.exception.code, "AGNIR_DISCOVERY_UNRESOLVABLE") - def test_orchestrator_checkpoints_explicit_update_through_agnir(self) -> None: + def test_orchestrator_checkpoints_explicit_update_through_agnir_0_1(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) write_project(root) @@ -140,15 +250,46 @@ def test_orchestrator_checkpoints_explicit_update_through_agnir(self) -> None: (root / ".agnir/state.md").read_text(encoding="utf-8"), "# State\nnew durable truth\n", ) - self.assertEqual( - (root / ".agnir/next-actions.md").read_text(encoding="utf-8"), - "# Next\ncontinue product integration\n", - ) evidence_files = list((root / ".agnir/evidence").glob("svif-operation-*.json")) self.assertEqual(len(evidence_files), 1) payload = json.loads(evidence_files[0].read_text(encoding="utf-8")) self.assertEqual(payload["operation_id"], "op-agnir-1") - self.assertEqual(payload["subject_identity"], SUBJECT) + self.assertIsNone(payload["agnir_lineage"]) + + def test_orchestrator_checkpoint_preserves_core_0_2_lineage_and_binding(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + selector = "refs/heads/feature/parallel" + lineage = "urn:test:lineage:parallel" + write_project(root, version="0.2", lineage=lineage, vcs_selector=selector) + provider = AgnirFilesystemContinuityProvider( + root, + expected_core_version="0.2", + expected_profile="repository-filesystem/0.2", + selected_vcs_selector=selector, + ) + surface = UpdatingSurface() + orchestrator = Orchestrator( + continuity_providers=(provider,), + execution_surfaces=(surface,), + ) + binding = ProjectBinding( + project_identity=PROJECT, + continuity=ProviderBinding("agnir"), + execution_surface="chatgpt", + ) + + orchestrator.run( + binding, + OperationRequest(operation_id="op-agnir-lineage", intent="advance selected lineage"), + ) + + self.assertEqual(provider.resolve_lineage(PROJECT), lineage) + evidence_files = list((root / ".agnir/evidence").glob("svif-operation-*.json")) + self.assertEqual(len(evidence_files), 1) + payload = json.loads(evidence_files[0].read_text(encoding="utf-8")) + self.assertEqual(payload["agnir_lineage"], lineage) + self.assertEqual(payload["operation_id"], "op-agnir-lineage") if __name__ == "__main__": diff --git a/tests/test_agnir_stable_migration.py b/tests/test_agnir_stable_migration.py new file mode 100644 index 0000000..0d1f7d2 --- /dev/null +++ b/tests/test_agnir_stable_migration.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +from svif.continuity.agnir import AgnirFilesystemContinuityProvider + + +ROOT = Path(__file__).resolve().parents[1] +PROJECT = "urn:svif:project:svif-core" +SKILL = ROOT / "plugin" / "skills" / "svif" / "SKILL.md" + + +class PublishedAgnirStableMigrationTests(unittest.TestCase): + def test_repository_self_consumes_published_core_0_2(self) -> None: + agnir = (ROOT / "AGNIR.yaml").read_text(encoding="utf-8") + svif = (ROOT / "SVIF.yaml").read_text(encoding="utf-8") + + self.assertIn('version: "0.2"', agnir) + self.assertIn('discovery_profile: "repository-filesystem/0.2"', agnir) + self.assertIn('release: "0.2.0"', agnir) + self.assertIn( + 'applied_revision: "fc84095ed5d500be9e1b43a4af0e93356571bbd4"', + agnir, + ) + self.assertIn('compatibility: "0.2"', svif) + self.assertIn('profile: "repository-filesystem/0.2"', svif) + + provider = AgnirFilesystemContinuityProvider( + ROOT, + expected_core_version="0.2", + expected_profile="repository-filesystem/0.2", + ) + snapshot = provider.load(PROJECT) + lineage = provider.resolve_lineage(PROJECT) + + self.assertEqual(snapshot.project_identity, PROJECT) + self.assertTrue(lineage) + self.assertIn("Svif", snapshot.state or "") + self.assertIn("Svif", snapshot.next_actions or "") + + lineage_match = re.search(r"^\s{2}lineage:\s*\"([^\"]+)\"", agnir, re.MULTILINE) + selector_match = re.search(r"^\s{6}selector:\s*\"([^\"]+)\"", agnir, re.MULTILINE) + self.assertIsNotNone(lineage_match) + self.assertIsNotNone(selector_match) + self.assertNotEqual(lineage_match.group(1), selector_match.group(1)) + self.assertIn(f'lineage: "{lineage_match.group(1)}"', svif) + self.assertIn(f'vcs_selector: "{selector_match.group(1)}"', svif) + + def test_migration_preserves_project_identity_and_memory_locators(self) -> None: + agnir = (ROOT / "AGNIR.yaml").read_text(encoding="utf-8") + for marker in ( + 'identity: "urn:svif:project:svif-core"', + 'state: ".agnir/state.md"', + 'next_actions: ".agnir/next-actions.md"', + 'decisions: ".agnir/decisions.md"', + 'evidence: ".agnir/evidence/"', + ): + self.assertIn(marker, agnir) + + def test_skill_distinguishes_current_self_host_from_released_preview_bootstrap(self) -> None: + text = SKILL.read_text(encoding="utf-8") + + for marker in ( + "For the current Svif repository binding, the expected values are Agnir Core `0.2`", + "profile `repository-filesystem/0.2`", + "one explicit logical Continuity Lineage", + "matching durable VCS selector binding", + "The released `v0.2.0-preview.1` first-use bootstrap remains", + "Core/profile `0.1` baseline", + "published Agnir repository release `v0.2.0`", + ): + self.assertIn(marker, text) + + self.assertIn("Initialize the Agnir `repository-filesystem/0.1` continuity contract", text) + self.assertIn('compatibility `"0.1"`', text) + self.assertIn("Core `0.2`, require a non-empty logical `continuity.lineage`", text) + self.assertIn("AGNIR_LINEAGE_REQUIRED", text) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_plugin_agnir_discovery.py b/tests/test_plugin_agnir_discovery.py index 8d7d26e..4e52c6e 100644 --- a/tests/test_plugin_agnir_discovery.py +++ b/tests/test_plugin_agnir_discovery.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re import unittest from pathlib import Path @@ -8,6 +9,13 @@ SKILL = ROOT / "plugin" / "skills" / "svif" / "SKILL.md" +def _quoted_scalar(text: str, key: str) -> str: + match = re.search(rf'^\s+{re.escape(key)}:\s+"([^"]+)"\s*$', text, re.MULTILINE) + if match is None: + raise AssertionError(f"missing quoted scalar {key!r}") + return match.group(1) + + class PluginAgnirDiscoveryTests(unittest.TestCase): def test_skill_requires_durable_agent_activation_route_before_discovery(self) -> None: text = SKILL.read_text(encoding="utf-8") @@ -98,10 +106,7 @@ def test_skill_requires_authority_to_select_one_project_root_before_discovery(se "a parent or child Project with its own `AGNIR.yaml` does not make that selected root ambiguous", text, ) - self.assertIn( - "MUST NOT be searched as a replacement", - text, - ) + self.assertIn("MUST NOT be searched as a replacement", text) def test_skill_selects_trusted_profile_before_resolving_discovery_record(self) -> None: text = SKILL.read_text(encoding="utf-8") @@ -282,21 +287,35 @@ def test_skill_surfaces_all_named_agnir_discovery_failures_without_fallback_sear self.assertIn("repair the earliest violated discovery invariant", text) self.assertIn("original authorized Project Entry Point", text) - def test_svif_binding_and_skill_agree_on_current_agnir_identity_and_compatibility(self) -> None: + def test_current_project_binding_can_migrate_without_rewriting_released_bootstrap_baseline(self) -> None: skill = SKILL.read_text(encoding="utf-8") agnir = (ROOT / "AGNIR.yaml").read_text(encoding="utf-8") svif = (ROOT / "SVIF.yaml").read_text(encoding="utf-8") + lineage = _quoted_scalar(svif, "lineage") + selector = _quoted_scalar(svif, "vcs_selector") - self.assertIn('version: "0.1"', agnir) - self.assertIn('discovery_profile: "repository-filesystem/0.1"', agnir) - self.assertIn('identity: "urn:svif:project:svif-core"', agnir) - self.assertIn('compatibility: "0.1"', svif) - self.assertIn('profile: "repository-filesystem/0.1"', svif) + for marker in ( + 'version: "0.2"', + 'discovery_profile: "repository-filesystem/0.2"', + 'identity: "urn:svif:project:svif-core"', + f'lineage: "{lineage}"', + f'selector: "{selector}"', + ): + self.assertIn(marker, agnir) + for marker in ( + 'compatibility: "0.2"', + 'profile: "repository-filesystem/0.2"', + f'lineage: "{lineage}"', + f'vcs_selector: "{selector}"', + ): + self.assertIn(marker, svif) + # The released Skills-only distribution still bootstraps new Projects on + # its published Agnir 0.1 baseline until a later distribution release says otherwise. for marker in ( "Agnir Core `0.1`", - "profile `repository-filesystem/0.1`", - "Project identity `urn:svif:project:svif-core`", + "repository-filesystem/0.1", + 'compatibility `"0.1"`', ): self.assertIn(marker, skill) diff --git a/tests/test_plugin_package.py b/tests/test_plugin_package.py index 52bb679..789b864 100644 --- a/tests/test_plugin_package.py +++ b/tests/test_plugin_package.py @@ -22,6 +22,13 @@ } +def _quoted_scalar(text: str, key: str) -> str: + match = re.search(rf'^\s+{re.escape(key)}:\s+"([^"]+)"\s*$', text, re.MULTILINE) + if match is None: + raise AssertionError(f"missing quoted scalar {key!r}") + return match.group(1) + + def validate_agent_plugins_1_0_manifest(manifest: object) -> tuple[list[str], list[str]]: """Return fatal errors and non-fatal diagnostics using Agent Plugins 1.0 rules. @@ -243,9 +250,7 @@ def test_manifest_validator_does_not_validate_unimplemented_extension_namespaces portable = { "$schema": SCHEMA_ID, "name": "svif", - "extensions": { - "com.example.client": "opaque-to-portable-validator", - }, + "extensions": {"com.example.client": "opaque-to-portable-validator"}, } errors, diagnostics = validate_agent_plugins_1_0_manifest(portable) self.assertEqual(errors, []) @@ -254,10 +259,7 @@ def test_manifest_validator_does_not_validate_unimplemented_extension_namespaces def test_plugin_package_paths_are_contained_within_plugin_root(self) -> None: result = inspect_plugin_filesystem(PLUGIN_ROOT) self.assertEqual(result, { - "plugin_errors": [], - "component_errors": [], - "skipped_skills": [], - "denied_paths": [], + "plugin_errors": [], "component_errors": [], "skipped_skills": [], "denied_paths": [], }) def test_manifest_escape_rejects_whole_plugin(self) -> None: @@ -268,7 +270,6 @@ def test_manifest_escape_rejects_whole_plugin(self) -> None: outside = base / "manifest.json" outside.write_text('{"$schema": "x", "name": "svif"}', encoding="utf-8") (plugin_root / "plugin.json").symlink_to(outside) - result = inspect_plugin_filesystem(plugin_root) self.assertTrue(any("plugin.json resolves outside" in error for error in result["plugin_errors"])) @@ -281,7 +282,6 @@ def test_skills_location_escape_invalidates_only_skill_component_type(self) -> N outside_skills = base / "outside-skills" outside_skills.mkdir() (plugin_root / "skills").symlink_to(outside_skills, target_is_directory=True) - result = inspect_plugin_filesystem(plugin_root) self.assertEqual(result["plugin_errors"], []) self.assertTrue(any("skills fixed component location escapes" in error for error in result["component_errors"])) @@ -296,7 +296,6 @@ def test_escaping_skill_is_skipped_without_rejecting_plugin(self) -> None: outside = base / "outside.md" outside.write_text("outside", encoding="utf-8") (skill_dir / "SKILL.md").symlink_to(outside) - result = inspect_plugin_filesystem(plugin_root) self.assertEqual(result["plugin_errors"], []) self.assertEqual(result["component_errors"], []) @@ -311,7 +310,6 @@ def test_unrelated_escape_is_denied_without_rejecting_plugin(self) -> None: outside = base / "outside.txt" outside.write_text("outside", encoding="utf-8") (plugin_root / "notes.txt").symlink_to(outside) - result = inspect_plugin_filesystem(plugin_root) self.assertEqual(result["plugin_errors"], []) self.assertIn("notes.txt", result["denied_paths"]) @@ -345,15 +343,28 @@ def test_svif_skill_has_required_core_guards(self) -> None: def test_current_agnir_binding_has_no_live_predecessor_ref(self) -> None: agnir = (ROOT / "AGNIR.yaml").read_text(encoding="utf-8") svif = (ROOT / "SVIF.yaml").read_text(encoding="utf-8") + lineage = _quoted_scalar(svif, "lineage") + selector = _quoted_scalar(svif, "vcs_selector") for text in (agnir, svif): self.assertNotIn("predecessor_ref:", text) self.assertNotIn("legacy/zerolocal-v0.1", text) - self.assertIn('discovery_profile: "repository-filesystem/0.1"', agnir) - self.assertIn('provider: "agnir"', svif) - self.assertIn('compatibility: "0.1"', svif) - self.assertIn('profile: "repository-filesystem/0.1"', svif) + for marker in ( + 'version: "0.2"', + 'discovery_profile: "repository-filesystem/0.2"', + f'lineage: "{lineage}"', + f'selector: "{selector}"', + ): + self.assertIn(marker, agnir) + for marker in ( + 'provider: "agnir"', + 'compatibility: "0.2"', + 'profile: "repository-filesystem/0.2"', + f'lineage: "{lineage}"', + f'vcs_selector: "{selector}"', + ): + self.assertIn(marker, svif) def test_svif_project_has_self_describing_agnir_cold_start_route(self) -> None: agents = (ROOT / "AGENTS.md").read_text(encoding="utf-8")