diff --git a/.agnir/evidence/2026-09-02-agnir-core-0.2-parallel-lineage.md b/.agnir/evidence/2026-09-02-agnir-core-0.2-parallel-lineage.md new file mode 100644 index 0000000..8fdf112 --- /dev/null +++ b/.agnir/evidence/2026-09-02-agnir-core-0.2-parallel-lineage.md @@ -0,0 +1,26 @@ +# Svif Agnir Core 0.2 parallel-lineage evidence — 2026-09-02 + +## Fork boundary + +This source lineage is forked from coherent Svif validation checkpoint `329984f94483a7cbbb21a6faa42b9cf9ed84fed2`. + +Project identity is preserved as `urn:svif:project:svif-core`. + +The source branch establishes a different logical Continuity Lineage and VCS selector binding: + +- lineage: `urn:svif:lineage:agnir-core-0.2-parallel`; +- selector: `refs/heads/feature/agnir-core-0.2-parallel`. + +The fork does not reinterpret the Git ref name or commit SHA as lineage identity. The ref is a selector/binding; the commit is a revision receipt. + +## Real Project divergence + +This source lineage advances a real Svif contract in `spec/PROJECT_BINDING.md`: provider-local parallel continuity may bind multiple independently advancing continuity contexts to one stable Svif Project identity, while provider-specific lineage/selector configuration remains outside the provider-neutral Svif Orchestrator kernel. + +The self-host test is generalized so it derives the currently selected lineage/selector from the Svif Project Binding and then verifies the real Agnir provider resolves the same logical lineage. This removes the previous target-lineage hard coding and makes the test valid for independently selected lineages of the same Project. + +## Publication rule + +The source fork must publish `AGNIR.yaml`, `SVIF.yaml`, source-local State / Next Actions, the real Project change, generalized self-host test, and this evidence in one coherent branch-advancing revision. The target validation lineage remains unchanged by this source checkpoint. + +A later source→target integration must still stage the integrated Project result without moving the target ref, reconcile target continuity, then publish the integrated target and reconciled target continuity together. diff --git a/.agnir/evidence/2026-09-02-agnir-core-0.2-real-consumer-validation-complete.md b/.agnir/evidence/2026-09-02-agnir-core-0.2-real-consumer-validation-complete.md new file mode 100644 index 0000000..2734d1a --- /dev/null +++ b/.agnir/evidence/2026-09-02-agnir-core-0.2-real-consumer-validation-complete.md @@ -0,0 +1,36 @@ +# Svif Agnir Core 0.2 real-consumer validation complete — 2026-09-02 + +## Outcome + +The first real Svif consumer validation of Agnir Core `0.2` / `repository-filesystem/0.2` completed on temporary branches without changing Svif `main`. + +Project identity remained `urn:svif:project:svif-core`. + +## Lineages and receipts + +- common coherent baseline: `329984f94483a7cbbb21a6faa42b9cf9ed84fed2`; +- target lineage `urn:svif:lineage:agnir-core-0.2-validation`, selector `refs/heads/feature/agnir-core-0.2-validation`; +- target pre-integration revision `79c5b7c7ee2ed545492702bea43d0f7135602f35`, CI `33619053159` success; +- source lineage `urn:svif:lineage:agnir-core-0.2-parallel`, selector `refs/heads/feature/agnir-core-0.2-parallel`; +- source revision `d2d0c1bf25526b54490cce14c5aa8797c85c4d54`, CI `33618885830` success; +- unpublished staged candidate `4b86b3adafe08cc2f7fd48eb4f685d2b633b25c3`; +- reconciled two-parent target `1cd25539c75f8a2a32c84b822c0db80b176fd319`; +- semantic self-host test repair `e48ae07faa6a716f7e2cd83cdcefdce6d02d8c7e`, CI `33619491154` 3/3 success. + +Target and source both made real, different Project changes. The staged candidate existed while fresh ref reads still showed the pre-integration target and source revisions. Its tree contained both Project changes but retained target lineage/selector binding. + +The target ref then advanced once from `79c5b7c7...` directly to the reconciled two-parent revision `1cd25539...`; the staged candidate was never target truth. + +## Verification finding + +The first post-publication run `33619306602` had repository-integrity and portable-contracts green and one runtime failure caused only by a self-host test asserting an obsolete workflow-stage heading. Commit `e48ae07...` changed that test to assert stable binding-driven semantics. Run `33619491154` then passed all three jobs. + +## Independent source resume + +After target publication and repair, source ref still resolved to `d2d0c1bf...`. Fresh source `AGNIR.yaml` still resolved Project `urn:svif:project:svif-core`, source lineage `urn:svif:lineage:agnir-core-0.2-parallel`, and the parallel selector. Source-local State remained intact. + +## Validated semantics + +The real Project validates explicit 0.1→0.2 migration, two independent lineages, lineage/selector/revision separation, binding-driven discovery, lineage-local checkpoints, staged integration without target publication, target reconciliation before publication, coherent target advancement, and independent source survival. Svif's generic Orchestrator remained Continuity-Provider-neutral. + +This evidence does not migrate Svif `main`, change released `v0.2.0-preview.1`, or publish Agnir Core `0.2`; it is input to Agnir release readiness. diff --git a/.agnir/evidence/2026-09-02-agnir-core-0.2-real-consumer-validation.md b/.agnir/evidence/2026-09-02-agnir-core-0.2-real-consumer-validation.md new file mode 100644 index 0000000..71b02dd --- /dev/null +++ b/.agnir/evidence/2026-09-02-agnir-core-0.2-real-consumer-validation.md @@ -0,0 +1,107 @@ +# Agnir Core 0.2 real-consumer validation — Svif — 2026-09-02 + +## Authorization and scope + +The Principal authorized continuing Agnir Core `0.2` work and using Svif as the first real consumer validation. Evidence is carried on temporary Svif branch `feature/agnir-core-0.2-validation`; authoritative Svif `main` remains unchanged during the experiment. + +Source baseline: + +- Svif `main`: `dac058789a27f32f4ed1949874c1954f31f12bd8`; +- Project identity: `urn:svif:project:svif-core`; +- baseline Agnir Core/profile: `0.1` / `repository-filesystem/0.1`; +- baseline stable Agnir operational release: `v0.1.1` at `e9712357ab590e5c1e5357b3cf3219d07d789aff`. + +Experimental Agnir source: + +- repository: `iorLab/agnir`; +- branch: `feature/core-0.2-lineage`; +- checkpoint revision: `414dba1e50ad1bdcae3ca91d19c6768fdaa030cc`; +- Core/profile candidate: `0.2` / `repository-filesystem/0.2`. + +## Explicit Project migration + +The validation branch migrates its provider-owned `AGNIR.yaml` explicitly rather than treating the Core-line change as a compatible upgrade. + +Selected migrated binding: + +- Project identity: `urn:svif:project:svif-core` (preserved); +- logical lineage: `urn:svif:lineage:agnir-core-0.2-validation`; +- VCS selector: `refs/heads/feature/agnir-core-0.2-validation`; +- existing State / Next Actions / Decisions / Evidence locators: preserved. + +`SVIF.yaml` constrains the founding Agnir Continuity Provider to compatibility `0.2`, profile `repository-filesystem/0.2`, and matching provider-specific lineage/selector configuration. + +## Real consumer implementation pressure + +The pre-validation Svif `AgnirFilesystemContinuityProvider` hard-coded Agnir Core/profile `0.1`. The real consumer therefore required an adapter change, not just manifest edits. + +The candidate adapter: + +- preserves Core/profile `0.1` support; +- recognizes Core/profile `0.2` as a distinct supported compatibility line; +- requires `continuity.lineage` on Core `0.2`; +- keeps logical lineage identity distinct from VCS selector binding; +- optionally constrains expected Core/profile from the Svif binding; +- fails explicit selected-selector binding absence/mismatch rather than guessing; +- records the resolved Agnir lineage in Svif runtime checkpoint evidence; +- keeps the generic Svif Orchestrator interface unchanged. + +Focused tests cover Core `0.2` discovery, binding mismatch/absence, compatibility mismatch, lineage requirement, and lineage-preserving checkpoint behavior while retaining Core `0.1` cases. + +## Draft PR and first CI pressure + +Svif Draft PR `#3` was opened from `feature/agnir-core-0.2-validation` to `main` only to exercise the real repository checks. The PR body explicitly states that green initial CI is not sufficient for merge. + +First migration commit: + +- `52c1e7c37c1678fd848c6a0ef1e9b36fedd3ce18` — `test: migrate Svif validation branch to Agnir Core 0.2`. + +CI run `33615826969` returned: + +- `portable-contracts`: **success**; +- `repository-integrity`: **failure** because the checker hard-coded `version: "0.1"` / `repository-filesystem/0.1`; +- `runtime-kernel`: **failure**, but every newly added Core `0.2` adapter test passed. Three old tests failed because two hard-coded the current Project binding as `0.1` and one expected still-valid release/public-distribution markers that had been dropped from the branch-local Next Actions rewrite. + +This was useful real-consumer evidence: the provider adapter itself handled Core `0.2`, while Project-level checks and continuity preservation still encoded single-compatibility assumptions. + +## Non-weakening repair + +Repair commit: + +- `891cbc4d980b90f86992bd2f3a48e326d49b505a` — `fix: preserve Svif gates across Core 0.2 migration`. + +The repair did not add historical `0.1` marker comments to trick string tests and did not skip repository-integrity. Instead: + +1. `checks/check_repository.py` now parses the current Agnir/Svif scalar binding and verifies coherence. It accepts only supported Core/profile pairs (`0.1`/`repository-filesystem/0.1` or `0.2`/`repository-filesystem/0.2`), requires Svif compatibility/profile to match Agnir, and for Core `0.2` requires matching logical lineage and selector binding while rejecting selector==lineage identity. +2. Plugin/discovery tests distinguish two legitimate layers: the current selected Project may be explicitly migrated to experimental Core `0.2`, while the released Skills-only first-use bootstrap remains on its published Core/profile `0.1` baseline until an intentional distribution release changes it. +3. `.agnir/next-actions.md` restores still-valid `v0.2.0-preview.1`, Codex CLI, ChatGPT desktop/Codex, immutable candidate, universal Plugins Directory, ChatGPT Web, and public/personal ChatGPT path work instead of erasing those obligations just because the active lineage focuses on Core `0.2`. + +## Green migrated baseline + +Follow-up CI run **`33616508143` completed successfully** on the repaired migrated branch: + +- `repository-integrity`: success; +- `portable-contracts`: success; +- `runtime-kernel`: success. + +This proves that the real Svif Project can consume the experimental Agnir Core/profile `0.2` binding without weakening the product's portable contracts, Plugin packaging/distribution checks, or existing Core `0.1` adapter/bootstrap coverage. + +## Fresh real-Project discovery checkpoint + +The next checkpoint adds `tests/test_agnir_core_0_2_self_host.py`, which loads the actual Svif repository root through the real `AgnirFilesystemContinuityProvider` with explicit Core/profile `0.2` and selector context. It verifies Project identity, logical lineage, and recovery of the real branch-local State / Next Actions / Evidence. + +The checkpoint also records run `33616508143` in branch-local State and preserves the complete release/distribution worklist. This creates the coherent source baseline from which a second logical lineage can be forked. + +## Remaining acceptance sequence + +Real-consumer validation is not complete until: + +1. the self-host checkpoint CI is green; +2. a second temporary branch is explicitly forked into a different logical lineage with the same Project identity; +3. source and target lineages diverge and checkpoint independently; +4. staged integration leaves the target ref unchanged while unreconciled; +5. target continuity is reconciled before target publication; +6. integrated target + source both fresh-resume coherently; +7. final evidence is fed back into Agnir Core `0.2` release readiness. + +No merge to Svif `main`, no stable Core `0.2` success claim, and no Agnir `v0.2.0` release claim should be made before that sequence is observed. diff --git a/.agnir/evidence/2026-09-02-agnir-core-0.2-reconciled-integration.md b/.agnir/evidence/2026-09-02-agnir-core-0.2-reconciled-integration.md new file mode 100644 index 0000000..8fe6084 --- /dev/null +++ b/.agnir/evidence/2026-09-02-agnir-core-0.2-reconciled-integration.md @@ -0,0 +1,75 @@ +# Svif Agnir Core 0.2 reconciled integration evidence — 2026-09-02 + +## Scope + +This evidence records the first real Svif two-lineage integration under the Agnir Core `0.2` candidate. + +Project identity throughout: `urn:svif:project:svif-core`. + +Target lineage: + +- logical lineage: `urn:svif:lineage:agnir-core-0.2-validation`; +- selector: `refs/heads/feature/agnir-core-0.2-validation`; +- pre-integration revision: `79c5b7c7ee2ed545492702bea43d0f7135602f35`; +- CI run: `33619053159` — repository-integrity, portable-contracts, runtime-kernel all success. + +Source lineage: + +- logical lineage: `urn:svif:lineage:agnir-core-0.2-parallel`; +- selector: `refs/heads/feature/agnir-core-0.2-parallel`; +- revision: `d2d0c1bf25526b54490cce14c5aa8797c85c4d54`; +- CI run: `33618885830` — repository-integrity, portable-contracts, runtime-kernel all success. + +Common coherent baseline: `329984f94483a7cbbb21a6faa42b9cf9ed84fed2`. + +## Independent divergence + +The lineages advanced independently from the common baseline. + +Target changed `ARCHITECTURE.md` and target-local continuity. Source changed `spec/PROJECT_BINDING.md`, generalized lineage/binding tests, and source-local continuity. The source and target therefore exercised real, non-identical Project work rather than merely changing lineage labels. + +## Staging without target publication + +A staged integration tree was built from the target tree plus source Project/test changes while deliberately retaining target `AGNIR.yaml`, target `SVIF.yaml`, and target continuity as the provisional target context. + +Unpublished two-parent staged candidate: + +- `4b86b3adafe08cc2f7fd48eb4f685d2b633b25c3`; +- first parent: target `79c5b7c7ee2ed545492702bea43d0f7135602f35`; +- second parent: source `d2d0c1bf25526b54490cce14c5aa8797c85c4d54`. + +After creating the candidate, fresh ref reads still observed: + +- target ref → `79c5b7c7ee2ed545492702bea43d0f7135602f35`; +- source ref → `d2d0c1bf25526b54490cce14c5aa8797c85c4d54`. + +This proves the integration candidate existed without advancing the target publication boundary. + +## Candidate content verification + +The staged candidate was inspected directly and showed both Project changes: + +- target Continuity Provider architecture text in `ARCHITECTURE.md`; +- source provider-local parallel-continuity contract in `spec/PROJECT_BINDING.md`. + +Its `AGNIR.yaml` still exposed target lineage `urn:svif:lineage:agnir-core-0.2-validation` and target selector `refs/heads/feature/agnir-core-0.2-validation`; source lineage/selector metadata was not copied into target binding truth. + +## Reconciliation + +The final target continuity is reconciled from: + +1. actual integrated Project candidate; +2. previous target continuity; +3. relevant source continuity and source evidence; +4. the Principal-authorized real-consumer validation intent; +5. observed source/target CI and ref stability. + +The reconciled target records both Project changes and the integration receipts while preserving target logical lineage identity and selector binding. The source lineage remains independently resumable and is not collapsed into target continuity. + +## Publication requirement + +The final target revision must use the same two parents as the staged candidate but a tree containing reconciled target continuity and this evidence. Only that final revision may advance `feature/agnir-core-0.2-validation`. + +The staged candidate itself is evidence, not authoritative target truth. + +After publication, full target CI and fresh target/source resume are still required before this real-consumer validation is considered complete. diff --git a/.agnir/evidence/2026-09-02-agnir-core-0.2-target-divergence.md b/.agnir/evidence/2026-09-02-agnir-core-0.2-target-divergence.md new file mode 100644 index 0000000..06b4055 --- /dev/null +++ b/.agnir/evidence/2026-09-02-agnir-core-0.2-target-divergence.md @@ -0,0 +1,17 @@ +# Svif Agnir Core 0.2 target divergence evidence — 2026-09-02 + +## Target-only advancement + +Target lineage `urn:svif:lineage:agnir-core-0.2-validation` on selector `refs/heads/feature/agnir-core-0.2-validation` advances independently from common checkpoint `329984f94483a7cbbb21a6faa42b9cf9ed84fed2`. + +Project identity remains `urn:svif:project:svif-core`. + +The target-only real Project change updates `ARCHITECTURE.md`: the generic Svif Orchestrator consumes an already selected continuity context and must not enumerate sibling provider contexts, infer Project identity from provider-local lineage/selector/revision metadata, or silently switch contexts when selection is unresolved. + +The source lineage remains a separate continuity line and carries a different real Project change in `spec/PROJECT_BINDING.md`. No source continuity or source selector metadata is published as target truth at this checkpoint. + +## Integration precondition + +This target checkpoint is accepted only after its product CI and fresh target self-host discovery pass. Staged source→target integration may begin only when the source lineage is independently green as well. + +At staging time the target ref must remain at this pre-integration checkpoint until target continuity has been reconciled against the actual integrated Project candidate, previous target truth, relevant source continuity/evidence, and Principal intent. diff --git a/.agnir/next-actions.md b/.agnir/next-actions.md index 486e09a..f16fbe2 100644 --- a/.agnir/next-actions.md +++ b/.agnir/next-actions.md @@ -1,72 +1,41 @@ # 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. +## After Agnir Core 0.2 real-consumer validation + +1. **Feed the completed Svif evidence into Agnir.** Record the exact common baseline, source/target revisions, staged candidate, reconciled two-parent target, post-publication verification repair, CI runs, and independent source fresh-resume in `iorLab/agnir` on `feature/core-0.2-lineage`. +2. **Reassess Agnir Core `0.2` release readiness.** The next protocol work should focus on safe integration of Agnir PR `#4` / `#5` into Agnir `main`, migration/release documentation, and `v0.2.0-rc.1` preparation rather than adding more synthetic lineage models unless the real evidence exposes a missing invariant. +3. **Do not ordinary-merge the experimental Svif PRs.** Source PR `#4` has been integrated into the temporary target through the Agnir-aware two-parent path and may be closed as completed-via-reconciliation once evidence is recorded. Target PR `#3` remains draft; authoritative Svif `main` adoption of Agnir Core `0.2` is a separate decision after Agnir publishes/accepts the compatibility line. +4. **Keep `v0.2.0-preview.1` immutable.** Preserve the released Preview's **immutable candidate** provenance and its real **Codex CLI** plus **ChatGPT desktop/Codex** acceptance evidence. Any Preview fix uses a new tag such as `v0.2.0-preview.2` rather than moving the released tag. +5. **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. +6. Keep live Cloudflare delivery disabled unless explicitly authorized. + +## Completed Agnir Core 0.2 validation receipts + +- Project: `urn:svif:project:svif-core`. +- Target lineage: `urn:svif:lineage:agnir-core-0.2-validation`. +- Target selector: `refs/heads/feature/agnir-core-0.2-validation`. +- Source lineage: `urn:svif:lineage:agnir-core-0.2-parallel`. +- Source selector: `refs/heads/feature/agnir-core-0.2-parallel`. +- Common baseline: `329984f94483a7cbbb21a6faa42b9cf9ed84fed2`. +- Target pre-integration: `79c5b7c7ee2ed545492702bea43d0f7135602f35`; CI `33619053159` success. +- Source: `d2d0c1bf25526b54490cce14c5aa8797c85c4d54`; CI `33618885830` success. +- Staged candidate: `4b86b3adafe08cc2f7fd48eb4f685d2b633b25c3`; never published as target truth. +- Reconciled two-parent target: `1cd25539c75f8a2a32c84b822c0db80b176fd319`. +- Initial post-publication verification: `33619306602`; only one brittle workflow-heading assertion failed. +- Semantic self-host repair: `e48ae07faa6a716f7e2cd83cdcefdce6d02d8c7e`; CI `33619491154` 3/3 success. +- Agnir experimental source revision used by Svif: `414dba1e50ad1bdcae3ca91d19c6768fdaa030cc`. + +## Invariants confirmed by the real Project + +- The Project persists while multiple Continuity Lineages advance independently. +- Project identity is not lineage identity; lineage identity is not selector or revision receipt. +- Selection is explicit/binding-driven and never guessed by sibling scanning. +- Checkpoints are lineage-local by default. +- Source continuity is reconciliation input, not automatic target truth. +- A staged integration candidate can exist while the target ref remains unchanged. +- Target continuity can be reconciled before target publication. +- Target ref advancement is the publication boundary and can publish integrated Project + reconciled target truth coherently. +- Source continuity remains independently resumable after target integration. +- Svif Orchestrator remains Continuity-Provider-neutral; lineage mechanics remain at the provider/binding boundary. +- The released Skills-only first-use bootstrap remains on its published Agnir Core/profile `0.1` baseline until an intentional distribution release changes it. +- `main` is the only long-lived authoritative Svif branch; validation branches are temporary evidence carriers. diff --git a/.agnir/state.md b/.agnir/state.md index d70d21e..dc406be 100644 --- a/.agnir/state.md +++ b/.agnir/state.md @@ -1,115 +1,48 @@ # 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 remains the authoritative active **Project orchestration product** in `iorLab/svif`; `main` remains the only long-lived authoritative branch. Agnir remains the founding **Continuity Provider**, ChatGPT a founding **Execution Surface**, and Cloudflare a founding **Capability Provider**. The former `iorLab/svif-cloudflare-reference` project is retired and historical only. -## Product architecture +## Agnir Core 0.2 real-consumer validation complete — 2026-09-02 -Svif coordinates four first-class components: +Temporary target branch `feature/agnir-core-0.2-validation` has completed the first real Svif consumer validation of Agnir Core `0.2` / `repository-filesystem/0.2` parallel continuity. -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`. +Project identity remained `urn:svif:project:svif-core` throughout. Target logical lineage remains `urn:svif:lineage:agnir-core-0.2-validation` bound to `refs/heads/feature/agnir-core-0.2-validation`. Source logical lineage remains independently available as `urn:svif:lineage:agnir-core-0.2-parallel` on `refs/heads/feature/agnir-core-0.2-parallel`. -Stable rule: +### Real two-lineage receipts -> The Project persists; Executors and execution environments may change. +- common coherent baseline: `329984f94483a7cbbb21a6faa42b9cf9ed84fed2`; +- independently advanced target: `79c5b7c7ee2ed545492702bea43d0f7135602f35`, CI `33619053159` 3/3 success; +- independently advanced source: `d2d0c1bf25526b54490cce14c5aa8797c85c4d54`, CI `33618885830` 3/3 success; +- unpublished staged two-parent candidate: `4b86b3adafe08cc2f7fd48eb4f685d2b633b25c3`; +- reconciled two-parent target revision: `1cd25539c75f8a2a32c84b822c0db80b176fd319`; +- post-publication semantic self-host test repair: `e48ae07faa6a716f7e2cd83cdcefdce6d02d8c7e`, CI `33619491154` 3/3 success. -No execution surface becomes canonical Project truth merely because execution occurred there. +The staged candidate existed while fresh ref reads still showed target at `79c5b7c7...` and source at `d2d0c1bf...`. The target ref then advanced exactly once from `79c5b7c7...` to reconciled two-parent revision `1cd25539...`; staged candidate `4b86b3ad...` was never authoritative target truth. -## Active contracts and released Preview +The first post-publication run `33619306602` had repository-integrity and portable-contracts green but one brittle self-host assertion expected an old workflow-stage heading. All actual Core `0.2` discovery and lineage checks passed. Commit `e48ae07...` replaced that heading check with binding-driven semantic assertions; run `33619491154` then passed repository-integrity, portable-contracts, and runtime-kernel. -- Svif product line: `0.2`; the current released Repository Preview is `0.2.0-preview.1`. -- 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`. +### Integrated real Project result -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. +The target now contains both independently developed Project changes: -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. +1. `ARCHITECTURE.md` states that the generic Svif Orchestrator consumes an already selected Continuity Context and must not enumerate/guess sibling provider contexts or infer Project identity from provider-local lineage/selector/revision metadata; +2. `spec/PROJECT_BINDING.md` states that one stable Svif Project may use multiple independently advancing provider-local continuity contexts while lineage/selector semantics remain provider-specific; +3. self-host and Plugin binding tests are binding-driven rather than hard-coding one lineage/selector. -## README entry architecture +Target `AGNIR.yaml` / `SVIF.yaml` remain target-bound. Source continuity is reconciliation evidence/input, not target truth. -The README front section is now deliberately layered before architecture material: +### Independent source fresh resume -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. +After target integration and target repair, a fresh read still observed source ref `feature/agnir-core-0.2-parallel` at `d2d0c1bf25526b54490cce14c5aa8797c85c4d54`. Its `AGNIR.yaml` still resolves Project `urn:svif:project:svif-core`, lineage `urn:svif:lineage:agnir-core-0.2-parallel`, and selector `refs/heads/feature/agnir-core-0.2-parallel`; its source-local State remains intact. Target publication did not rewrite or collapse source continuity. -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. +## Conclusion -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. +The real Svif consumer experiment validates migration, explicit lineage binding, independent divergence, lineage-local checkpoints, binding-driven fresh resume, staged integration without target publication, target reconciliation, coherent target advancement, and independent source survival on a real Project. -## Runtime baseline +This success does **not** migrate Svif authoritative `main` or publish Agnir Core `0.2` stable. Svif `main` and released `v0.2.0-preview.1` remain unchanged. The result should now be fed back into Agnir `feature/core-0.2-lineage` as release-readiness evidence. -- 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. +## Product/distribution state preserved -`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. +The Plugin MVP remains active. `README.md` and `README.zh-CN.md` remain synchronized entry points. Released `v0.2.0-preview.1` remains immutable with prior Codex CLI and ChatGPT desktop/Codex acceptance evidence. The public/personal ChatGPT Web / universal Plugins Directory path remains separate active work. Live Cloudflare delivery remains disabled unless explicitly authorized. -## Plugin MVP and first-use onboarding - -Svif has a released **Skills-only `v0.2.0-preview.1` Plugin Preview** under `plugin/`: - -- `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. - -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. - -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. - -## Repository Preview distribution status - -The copy-ready user intent remains exactly: - -`Install and enable Svif for this Project: https://github.com/iorLab/svif` - -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. - -## Personal ChatGPT public distribution status - -The primary ChatGPT audience is individual/personal users. The mature consumer path is: - -`individual ChatGPT user -> universal Plugins Directory -> install -> invoke Svif in normal ChatGPT use` - -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. - -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. - -## Current resume point - -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 checkpoints - -- 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. - -`.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 resume order; `.agnir/decisions.md` remains authoritative for durable architecture and distribution decisions. diff --git a/AGNIR.yaml b/AGNIR.yaml index 3c94e5b..d87be4d 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:agnir-core-0.2-validation" + memory: state: ".agnir/state.md" next_actions: ".agnir/next-actions.md" @@ -20,8 +23,17 @@ 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/feature/agnir-core-0.2-validation" + agnir/experiments: + purpose: "svif-real-core-0.2-consumer-validation" + source_revision: "414dba1e50ad1bdcae3ca91d19c6768fdaa030cc" agnir/operations: distribution: "agnir-agent-skill" - release: "0.1.1" + release: "0.2.0-dev" source: "iorLab/agnir" - applied_revision: "e9712357ab590e5c1e5357b3cf3219d07d789aff" + applied_revision: "414dba1e50ad1bdcae3ca91d19c6768fdaa030cc" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 728a68d..b04b423 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -49,7 +49,11 @@ ChatGPT uses the externally driven form. Untrusted model/result payloads cannot A Continuity Provider supplies durable Project truth and resumability. The Svif kernel depends on this interface, not permanently on Agnir. -`src/svif/continuity/agnir.py` is the founding adapter for Agnir Core `0.1` repository/filesystem discovery and checkpoint semantics. +`src/svif/continuity/agnir.py` is the founding adapter for Agnir repository/filesystem discovery and checkpoint semantics. + +A Continuity Provider may expose more than one provider-local continuity context for the same stable Svif Project identity when its own contract supports that behavior. The **selected** continuity context comes from the Project Binding or another trusted adapter context; the generic Orchestrator consumes the selected `ContinuitySnapshot` and MUST NOT enumerate sibling provider contexts, infer one from a backend branch/ref/revision, or silently switch contexts when selection is missing or inconsistent. + +Provider-local lineage, namespace, selector, revision, and reconciliation semantics therefore remain at the Continuity Provider / Project Binding boundary. They do not become generic Svif Project identity or Orchestrator lifecycle semantics. ## 5. Execution Surface diff --git a/SVIF.yaml b/SVIF.yaml index d7e360f..81dd30c 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:agnir-core-0.2-validation" + vcs_selector: "refs/heads/feature/agnir-core-0.2-validation" 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_core_0_2_self_host: "tests/test_agnir_core_0_2_self_host.py" chatgpt_surface: "tests/test_chatgpt_surface.py" cloudflare_capability: "tests/test_cloudflare_capability.py" founding_e2e: "tests/test_founding_e2e.py" @@ -56,3 +59,5 @@ extensions: svif/repository: canonical: "iorLab/svif" authoritative_ref: "main" + svif/experiments: + agnir_core_0_2_real_consumer_validation: true diff --git a/checks/check_repository.py b/checks/check_repository.py index dc512e4..931f5bd 100644 --- 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/spec/PROJECT_BINDING.md b/spec/PROJECT_BINDING.md index 932487a..4dbecae 100644 --- a/spec/PROJECT_BINDING.md +++ b/spec/PROJECT_BINDING.md @@ -40,9 +40,12 @@ project: bindings: continuity: provider: "agnir" - compatibility: "0.1" + compatibility: "0.2" + profile: "repository-filesystem/0.2" config: discovery: "AGNIR.yaml" + lineage: "urn:example:lineage:main" + vcs_selector: "refs/heads/main" execution: - surface: "chatgpt" @@ -61,15 +64,29 @@ The arrays may be empty when a Project does not bind an Execution Surface or Cap `bindings.continuity` MUST identify `provider`. -`compatibility` MAY constrain the provider contract/version. `config` MAY carry non-secret provider-specific discovery/configuration. +`compatibility` MAY constrain the provider contract/version. `profile` MAY constrain a provider-specific profile. `config` MAY carry non-secret provider-specific discovery, lineage-selection, selector-binding, or adapter configuration. -For the active founding integration: +Svif MUST treat these fields as provider-specific constraints; it MUST NOT infer that every Continuity Provider supports Agnir compatibility lines, logical lineages, files, Git, or repositories. + +For the founding Agnir integration: - provider: `agnir`; -- compatibility: `0.1`; -- repository/filesystem discovery may point to `AGNIR.yaml`. +- the released Svif `v0.2.0-preview.1` line consumes Agnir Core `0.1` / `repository-filesystem/0.1`; +- the active Core `0.2` real-consumer validation may bind Agnir Core `0.2` / `repository-filesystem/0.2` explicitly; +- repository/filesystem discovery points to `AGNIR.yaml`; +- when Core `0.2` is selected, a provider-specific logical lineage identity and VCS selector binding MAY be supplied in `config` and MUST agree with the selected Agnir Project root/binding. + +A compatibility-line change such as Agnir Core `0.1` → `0.2` is a Continuity Provider migration, not a normal compatible operational upgrade. Svif MUST preserve Project identity and provider-owned durable truth while the provider performs that migration. + +### Provider-local parallel continuity + +A Continuity Provider MAY expose multiple independently advancing continuity contexts for one stable Svif Project identity when the selected provider compatibility/profile supports that behavior. + +Svif Project identity MUST NOT be derived from a provider lineage, namespace, branch, selector, revision, worktree, or checkpoint receipt. Provider-specific lineage identity and backend selector/binding metadata remain inside `bindings.continuity.config` (or an equivalent provider-specific binding object) and MUST NOT become generic Orchestrator identity semantics. + +Two Svif bindings may therefore identify the same `project.identity` and the same Continuity Provider while selecting different provider-local continuity lineages. Each selected binding must be self-consistent and independently resumable according to that provider's contract. Svif MUST NOT scan sibling provider contexts and guess which one is current when the selected binding is absent, inconsistent, or unresolved. -Svif kernel semantics MUST NOT infer that all Continuity Providers use Agnir, files, Git, or repositories. +When provider-local lineages are integrated, the Continuity Provider owns reconciliation and coherent publication according to its contract. Svif MUST NOT copy the source lineage's provider-specific selector or continuity metadata into the target merely because Project content was integrated. ## 5. Execution Surface bindings 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_core_0_2_self_host.py b/tests/test_agnir_core_0_2_self_host.py new file mode 100644 index 0000000..82e364c --- /dev/null +++ b/tests/test_agnir_core_0_2_self_host.py @@ -0,0 +1,48 @@ +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" + + +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 SelfHostedAgnirCore02Tests(unittest.TestCase): + def test_real_svif_project_fresh_resolves_selected_core_0_2_lineage(self) -> None: + svif = (ROOT / "SVIF.yaml").read_text(encoding="utf-8") + expected_lineage = _quoted_scalar(svif, "lineage") + expected_selector = _quoted_scalar(svif, "vcs_selector") + + provider = AgnirFilesystemContinuityProvider( + ROOT, + expected_core_version="0.2", + expected_profile="repository-filesystem/0.2", + selected_vcs_selector=expected_selector, + ) + + snapshot = provider.load(PROJECT) + + self.assertEqual(snapshot.project_identity, PROJECT) + self.assertEqual(provider.resolve_lineage(PROJECT), expected_lineage) + self.assertIn("Project orchestration product", snapshot.state) + self.assertIn("Agnir Core 0.2", snapshot.next_actions) + self.assertIn(expected_lineage, snapshot.next_actions) + self.assertTrue( + any("agnir-core-0.2" in name for name in snapshot.evidence), + "fresh resume should recover inspectable Core 0.2 evidence", + ) + + +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")