diff --git a/.github/workflows/ecosystem-integration.yml b/.github/workflows/ecosystem-integration.yml index cd07d888..ea96485d 100644 --- a/.github/workflows/ecosystem-integration.yml +++ b/.github/workflows/ecosystem-integration.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: repository: 8Dionysus/aoa-skills - ref: 9b009123e88cd864ef11147a6d9cba31a3bf81a5 + ref: 86720fe6aed8c6d59a8d938209032787f2670d09 path: .deps/aoa-skills token: ${{ secrets.AOA_ECOSYSTEM_READ_TOKEN || github.token }} diff --git a/DESIGN.AGENTS.md b/DESIGN.AGENTS.md index feae19a6..67a726da 100644 --- a/DESIGN.AGENTS.md +++ b/DESIGN.AGENTS.md @@ -22,6 +22,14 @@ Architecture belongs to `DESIGN.md`. Commands and recovery procedures belong to `PIPELINE.md`. Naming belongs to `NAMING.md`. Current status belongs to live diagnostics. +The two advertised skill routers describe operations and required +capabilities. `skills/port.manifest.json` keeps admitted owner packages +separate from consumer exposures; the current Codex user profile is one +explicit exposure. A reader may inspect a neutral package without installing +it. Model or reasoning configuration does not change its evidence, authority, +or result contract. Codex hook events, JSONL ingress, app-server calls, and +live compaction remain under `session-memory.adapters.codex`. + ## Agent Access Thesis An agent should receive the smallest evidence-bearing packet that can answer diff --git a/INSTALL.md b/INSTALL.md index cc4db93d..c444a38b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -105,6 +105,64 @@ explicit user-level installation. Other bundle skills stay local as focused procedures. User skill links are host state and are not part of portable source readiness. +## Scoped session-memory skill interface overlay + +When an existing workspace runtime has a valid +`diagnostics/install-profile.json`, a source checkout may be applied through +the bounded skill-interface installer. This route updates the capability home, +generated capability read models, the two advertised session-memory skill +packages, and their package metadata. It does not run the full kernel installer +or rewrite the base install profile, sessions, generated runtime stores, maps, +hooks, systemd files, or user-level skill links. + +The source checkout and the external `aoa-skills` contract checkout are named +explicitly. The source must be a clean Git checkout and pass the owner +validator before any target path is staged: + +```bash +python3 scripts/install_skill_interface.py check \ + --source-aoa-root /absolute/path/to/source-checkout \ + --skills-root /absolute/path/to/aoa-skills \ + --workspace-root /absolute/path/to/workspace \ + --aoa-root /absolute/path/to/workspace/.aoa +``` + +Review the JSON plan, then authorize replacement of the selected component with +`install --force` (the `execute` spelling is an alias): + +```bash +python3 scripts/install_skill_interface.py install --force \ + --source-aoa-root /absolute/path/to/source-checkout \ + --skills-root /absolute/path/to/aoa-skills \ + --workspace-root /absolute/path/to/workspace \ + --aoa-root /absolute/path/to/workspace/.aoa +``` + +The default closure contains the global router and evidence route. A further +graph-declared skill can be added by repeating `--skill `; each selected +package is copied recursively. The installer records a separate +`diagnostics/skill-projection-install.json` receipt and a durable bounded +backup. Its claim is limited to source provenance and selected-byte parity; +the receipt does not prove prompt selection, invocation, route quality, runtime +health, or downstream outcomes. + +Rollback is an explicit receipt-revalidated operation and remains available +when the candidate source or `aoa-skills` checkout is unavailable: + +```bash +python3 scripts/install_skill_interface.py rollback \ + --workspace-root /absolute/path/to/workspace \ + --aoa-root /absolute/path/to/workspace/.aoa +``` + +Rollback verifies the receipt, base profile anchor, selected closed allowlist, +current after-state, and durable backup before restoring the selected paths and +the previous component receipt. `--source-aoa-root` and `--skills-root` may be +supplied for optional receipt identity checks; rollback never re-runs the +source or external owner validator. It leaves the kernel profile, sessions, +stores, maps, hooks, systemd files, and user skill links under their existing +owners. + ## Validation after install Source validation, installed-root health, and completion audit are different diff --git a/capabilities/families/session-memory.yaml b/capabilities/families/session-memory.yaml index 3ae61047..f4da65b7 100644 --- a/capabilities/families/session-memory.yaml +++ b/capabilities/families/session-memory.yaml @@ -137,7 +137,7 @@ nodes: - id: skill.aoa-session-memory-global-route title: Global session-memory router - description: Use in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation. + description: "Use when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation." purpose: Select the narrowest owner capability and preserve source, runtime, and proof boundaries. kind: skill contract_level: executable @@ -148,7 +148,7 @@ nodes: authority: authored lifecycle: state: experimental - version: 1.0.0 + version: 1.0.1 visibility: advertised evidence_state: manual-baseline health: challenger @@ -203,7 +203,7 @@ nodes: authority: authored lifecycle: &advertised_lifecycle state: experimental - version: 1.0.0 + version: 1.0.1 visibility: advertised evidence_state: manual-baseline health: challenger diff --git a/docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md b/docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md new file mode 100644 index 00000000..569701ac --- /dev/null +++ b/docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md @@ -0,0 +1,108 @@ +# Independent Skill Interface Delivery + +## Status + +Accepted. + +## Index Metadata + +- Decision ID: AOA-SM-D-0102 +- Original date: 2026-09-08 +- Owner surfaces: `skills/`, `capabilities/`, `generated/`, `scripts/install_skill_interface.py`, `INSTALL.md`, `tests/` +- Surface classes: installation, skill routing, portability, source lineage +- Projection layers: capability graph, skill router, component install receipt +- Guard families: bounded mutation, package provenance, source authority, rollback, kernel preservation +- Posture: accepted component delivery boundary + +## Context + +The capability home and its skill packages can evolve independently of an +active capture and retrieval kernel. The full portable installer replaces +kernel source and records its provenance. Using that route solely to deliver +skill instructions couples two independently owned changes and can overwrite +an intentionally different active kernel. Copying only the entrypoint has the +opposite defect: manifests, references, and generated routing retain old +package identities. + +## Options Considered + +- Always reinstall the portable kernel. Rejected for an interface-only change + because it replaces unrelated executable source and its install provenance. +- Copy or redirect the two user entrypoints. Rejected because this detaches + active-root ownership or leaves the capability graph inconsistent. +- Deliver the complete selected skill interface as a separately reversible + owner operation, anchored to the existing kernel profile. Accepted. + +## Decision + +`scripts/install_skill_interface.py` owns a bounded interface overlay. Its +default selection is the two advertised routers; additional skill packages +must be explicitly selected from the owner capability graph. The fixed shared +closure contains capability declarations, the skill home manifest and owner +guidance, generated graph/readout, and the global router card. Selected skill +directories move as complete packages. + +Source admission requires a clean Git identity and validation through an +explicitly selected `aoa-skills` contract checkout. Every unselected skill +package referenced by the new graph must already match the target. Kernel +source equality is not a prerequisite: the valid existing kernel install +profile is a preservation anchor, not provenance for the new interface. + +The operation checks source and target identity before replacement, preserves +a durable bounded backup, and publishes its component receipt last. Failures +before mutation preserve concurrent changes. Recovery restores only the +operation's admitted component; changed or unverifiable rollback evidence +blocks restoration. A later full install changes the base-profile anchor and +makes an older component receipt stale. + +User-skill links continue to target the active owner root. Their aggregate +profile receipt is checked and refreshed by the existing profile assembler. +The component installer does not own that receipt or the user's catalog. + +## Rationale + +Separate provenance allows a current interface to coexist honestly with a +different active kernel. Full package and graph parity preserves D-0018's +source/read-model boundary, while the unchanged kernel profile preserves the +executable lineage established by D-0097. Explicit contract binding makes +shared schema drift visible without substituting a nearby checkout. + +## Consequences + +- Interface changes can be delivered and reversed without a kernel restart. +- A component receipt describes selected bytes and their source identity; + kernel provenance remains in the full install profile. +- Shared graph changes may require selecting additional affected packages; + the operation must not silently broaden its selection. +- Recovery retains bounded backup storage until its disposition is chosen. + +## Boundaries + +This route does not alter sessions, raw evidence, capture hooks, runtime maps, +search stores, system services, or model configuration. It grants no exposure, +publication, execution, or promotion authority. Valid source and receipt data +do not prove prompt selection, successful invocation, runtime health, or +cross-host portability. + +## Source Surfaces + +- `scripts/install_skill_interface.py` +- `INSTALL.md` +- `capabilities/port.manifest.json` +- `skills/port.manifest.json` +- `tests/test_install_skill_interface.py` + +## Follow-Up Route + +Apply the owner component check/install/rollback path, then the profile +assembler and a fresh consumer read. Keep source validation, merge, selected +delivery, catalog visibility, and observed behavior as separate claims. + +## Verification + +Focused fixtures cover complete selected package delivery, unselected skill +parity, kernel/profile and archive preservation, stale identities, source and +target changes before commit, failed replacement recovery, and exact rollback +evidence. Run capability generated parity and portable export checks through +their owners. Live delivery and rollback evidence belongs to the bounded +operation record, not this durable decision. diff --git a/docs/decisions/indexes/by-date.md b/docs/decisions/indexes/by-date.md index 477f14de..5b733408 100644 --- a/docs/decisions/indexes/by-date.md +++ b/docs/decisions/indexes/by-date.md @@ -4,6 +4,7 @@ | Date | Decision | Status | Path | | --- | --- | --- | --- | +| 2026-09-08 | [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | | 2026-08-26 | [AOA-SM-D-0100 Bounded Fair Consumer Retirement Admission](../AOA-SM-D-0100-bounded-fair-consumer-retirement-admission.md) | Accepted | `docs/decisions/AOA-SM-D-0100-bounded-fair-consumer-retirement-admission.md` | | 2026-08-26 | [AOA-SM-D-0099 Terminal Outbox Retirement and Staged Freshness Retry](../AOA-SM-D-0099-terminal-outbox-retirement-and-staged-freshness-retry.md) | Accepted | `docs/decisions/AOA-SM-D-0099-terminal-outbox-retirement-and-staged-freshness-retry.md` | | 2026-08-26 | [AOA-SM-D-0098 Bounded Generated-Reader Process Boundary](../AOA-SM-D-0098-bounded-generated-reader-process-boundary.md) | Accepted | `docs/decisions/AOA-SM-D-0098-bounded-generated-reader-process-boundary.md` | diff --git a/docs/decisions/indexes/by-guard.md b/docs/decisions/indexes/by-guard.md index bfb92692..c208789a 100644 --- a/docs/decisions/indexes/by-guard.md +++ b/docs/decisions/indexes/by-guard.md @@ -254,6 +254,12 @@ | --- | --- | --- | --- | | [AOA-SM-D-0017 Bounded Temporal Interval Reading Before Answer Admission](../AOA-SM-D-0017-bounded-temporal-interval-reading-before-answer-admission.md) | 2026-07-18 | Accepted | `docs/decisions/AOA-SM-D-0017-bounded-temporal-interval-reading-before-answer-admission.md` | +## bounded mutation + +| Decision | Date | Status | Path | +| --- | --- | --- | --- | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | + ## bounded producer contract | Decision | Date | Status | Path | @@ -1217,6 +1223,12 @@ | --- | --- | --- | --- | | [AOA-SM-D-0070 Coalesced Hook Intent and Contention-Neutral Retry](../AOA-SM-D-0070-coalesced-hook-intent-and-contention-neutral-retry.md) | 2026-08-13 | Accepted | `docs/decisions/AOA-SM-D-0070-coalesced-hook-intent-and-contention-neutral-retry.md` | +## kernel preservation + +| Decision | Date | Status | Path | +| --- | --- | --- | --- | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | + ## last-good fallback | Decision | Date | Status | Path | @@ -1403,6 +1415,7 @@ | Decision | Date | Status | Path | | --- | --- | --- | --- | | [AOA-SM-D-0018 Owner Capability Home And Skill Evidence Lifecycle](../AOA-SM-D-0018-owner-capability-home-and-skill-evidence-lifecycle.md) | 2026-07-18 | Accepted | `docs/decisions/AOA-SM-D-0018-owner-capability-home-and-skill-evidence-lifecycle.md` | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | ## parent-death @@ -1839,6 +1852,7 @@ | Decision | Date | Status | Path | | --- | --- | --- | --- | | [AOA-SM-D-0023 Manifest-Committed Graph Sidecar Publication](../AOA-SM-D-0023-manifest-committed-graph-sidecar-publication.md) | 2026-07-19 | Accepted | `docs/decisions/AOA-SM-D-0023-manifest-committed-graph-sidecar-publication.md` | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | ## round-robin fairness @@ -1989,6 +2003,7 @@ | Decision | Date | Status | Path | | --- | --- | --- | --- | | [AOA-SM-D-0028 Authoritative Registry Rebuild Excludes Generated History](../AOA-SM-D-0028-authoritative-registry-rebuild-excludes-generated-history.md) | 2026-07-20 | Accepted | `docs/decisions/AOA-SM-D-0028-authoritative-registry-rebuild-excludes-generated-history.md` | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | ## source epoch diff --git a/docs/decisions/indexes/by-number.md b/docs/decisions/indexes/by-number.md index 570bf13c..8781edd2 100644 --- a/docs/decisions/indexes/by-number.md +++ b/docs/decisions/indexes/by-number.md @@ -105,3 +105,4 @@ | [AOA-SM-D-0099 Terminal Outbox Retirement and Staged Freshness Retry](../AOA-SM-D-0099-terminal-outbox-retirement-and-staged-freshness-retry.md) | 2026-08-26 | Accepted | scripts/aoa_session_memory.py, schemas/projection-outbox-retirement.schema.json, DESIGN.md, DESIGN.AGENTS.md, PIPELINE.md, INSTALL.md, READINESS.md, tests/test_session_memory.py | projection outbox, freshness orchestration, terminal proof | episode semantic, exact search, entity registry, graph | immutable work intent, exact completion receipt, retirement binding, staged retry | accepted | `docs/decisions/AOA-SM-D-0099-terminal-outbox-retirement-and-staged-freshness-retry.md` | | [AOA-SM-D-0100 Bounded Fair Consumer Retirement Admission](../AOA-SM-D-0100-bounded-fair-consumer-retirement-admission.md) | 2026-08-26 | Accepted | scripts/aoa_session_memory.py, schemas/, tests/, docs/decisions/ | projection outbox, freshness orchestration, scheduling, evidence boundary | exact search, episode semantic, entity registry, graph | fair scheduling, restart safety, immutable identity, fail-closed admission, terminal retirement | accepted | `docs/decisions/AOA-SM-D-0100-bounded-fair-consumer-retirement-admission.md` | | [AOA-SM-D-0101 Targeted Projection Outbox Consumers Reconcile One Exact Obligation](../AOA-SM-D-0101-targeted-outbox-consumer-reconciliation.md) | 2026-08-23 | Accepted | scripts/aoa_session_memory.py, tests/, docs/decisions/ | projection, freshness, orchestration, evidence boundary | component-delta outbox, exact consumer state, retirement | immutable identity, allowlisted route, dependency order, resource admission, bounded retry | accepted | `docs/decisions/AOA-SM-D-0101-targeted-outbox-consumer-reconciliation.md` | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | skills/, capabilities/, generated/, scripts/install_skill_interface.py, INSTALL.md, tests/ | installation, skill routing, portability, source lineage | capability graph, skill router, component install receipt | bounded mutation, package provenance, source authority, rollback, kernel preservation | accepted component delivery boundary | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | diff --git a/docs/decisions/indexes/by-projection-layer.md b/docs/decisions/indexes/by-projection-layer.md index cc887b2b..8a4e58e4 100644 --- a/docs/decisions/indexes/by-projection-layer.md +++ b/docs/decisions/indexes/by-projection-layer.md @@ -88,6 +88,12 @@ | --- | --- | --- | --- | | [AOA-SM-D-0013 Indexed Graph Seed Before Retrieval Fallback](../AOA-SM-D-0013-indexed-graph-seed-before-retrieval-fallback.md) | 2026-07-17 | Accepted | `docs/decisions/AOA-SM-D-0013-indexed-graph-seed-before-retrieval-fallback.md` | +## capability graph + +| Decision | Date | Status | Path | +| --- | --- | --- | --- | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | + ## capture-time envelope adapter | Decision | Date | Status | Path | @@ -119,6 +125,12 @@ | --- | --- | --- | --- | | [AOA-SM-D-0040 Served-Request Health Is Distinct from Provider Availability](../AOA-SM-D-0040-served-request-health-is-distinct-from-provider-availability.md) | 2026-07-31 | Accepted | `docs/decisions/AOA-SM-D-0040-served-request-health-is-distinct-from-provider-availability.md` | +## component install receipt + +| Decision | Date | Status | Path | +| --- | --- | --- | --- | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | + ## component-delta outbox | Decision | Date | Status | Path | @@ -1144,6 +1156,12 @@ | --- | --- | --- | --- | | [AOA-SM-D-0014 Session-Scoped Structured Prompt-Visibility Probe](../AOA-SM-D-0014-session-scoped-structured-prompt-visibility-probe.md) | 2026-07-17 | Accepted | `docs/decisions/AOA-SM-D-0014-session-scoped-structured-prompt-visibility-probe.md` | +## skill router + +| Decision | Date | Status | Path | +| --- | --- | --- | --- | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | + ## stable session projection | Decision | Date | Status | Path | diff --git a/docs/decisions/indexes/by-surface.md b/docs/decisions/indexes/by-surface.md index 827c2c0c..a0d4679f 100644 --- a/docs/decisions/indexes/by-surface.md +++ b/docs/decisions/indexes/by-surface.md @@ -526,6 +526,7 @@ | [AOA-SM-D-0094 Logical Registry Coverage and Capture-Only Ingress Separation](../AOA-SM-D-0094-logical-registry-coverage-and-capture-only-ingress.md) | 2026-08-22 | Accepted | `docs/decisions/AOA-SM-D-0094-logical-registry-coverage-and-capture-only-ingress.md` | | [AOA-SM-D-0096 Source-Owned Persistent Retry Dispatch Activation](../AOA-SM-D-0096-source-owned-persistent-retry-dispatch-activation.md) | 2026-08-24 | Accepted | `docs/decisions/AOA-SM-D-0096-source-owned-persistent-retry-dispatch-activation.md` | | [AOA-SM-D-0097 Source Install Provenance and Bounded Global Exact Recovery](../AOA-SM-D-0097-source-install-provenance-and-bounded-global-exact-recovery.md) | 2026-08-25 | Accepted | `docs/decisions/AOA-SM-D-0097-source-install-provenance-and-bounded-global-exact-recovery.md` | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | ## instrumentation @@ -670,6 +671,7 @@ | --- | --- | --- | --- | | [AOA-SM-D-0018 Owner Capability Home And Skill Evidence Lifecycle](../AOA-SM-D-0018-owner-capability-home-and-skill-evidence-lifecycle.md) | 2026-07-18 | Accepted | `docs/decisions/AOA-SM-D-0018-owner-capability-home-and-skill-evidence-lifecycle.md` | | [AOA-SM-D-0054 Native-Assisted Portable SHA-256 Continuation](../AOA-SM-D-0054-native-assisted-portable-sha256-continuation.md) | 2026-08-11 | Accepted | `docs/decisions/AOA-SM-D-0054-native-assisted-portable-sha256-continuation.md` | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | ## portable export @@ -1075,12 +1077,14 @@ | Decision | Date | Status | Path | | --- | --- | --- | --- | | [AOA-SM-D-0018 Owner Capability Home And Skill Evidence Lifecycle](../AOA-SM-D-0018-owner-capability-home-and-skill-evidence-lifecycle.md) | 2026-07-18 | Accepted | `docs/decisions/AOA-SM-D-0018-owner-capability-home-and-skill-evidence-lifecycle.md` | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | ## source lineage | Decision | Date | Status | Path | | --- | --- | --- | --- | | [AOA-SM-D-0097 Source Install Provenance and Bounded Global Exact Recovery](../AOA-SM-D-0097-source-install-provenance-and-bounded-global-exact-recovery.md) | 2026-08-25 | Accepted | `docs/decisions/AOA-SM-D-0097-source-install-provenance-and-bounded-global-exact-recovery.md` | +| [AOA-SM-D-0102 Independent Skill Interface Delivery](../AOA-SM-D-0102-independent-skill-interface-delivery.md) | 2026-09-08 | Accepted | `docs/decisions/AOA-SM-D-0102-independent-skill-interface-delivery.md` | ## source-aware admission diff --git a/docs/validation/validation_lanes.json b/docs/validation/validation_lanes.json index 1ac090be..aa4d98aa 100644 --- a/docs/validation/validation_lanes.json +++ b/docs/validation/validation_lanes.json @@ -67,6 +67,7 @@ "tests/test_session_memory_episode_temporal.py", "tests/test_session_memory_capture.py", "tests/test_session_memory_sweep.py", + "tests/test_install_skill_interface.py", "tests/test_public_tree_audit.py", "tests/test_git_history_audit.py" ] diff --git a/generated/capability_graph.json b/generated/capability_graph.json index b03a32c4..9c5b6e30 100644 --- a/generated/capability_graph.json +++ b/generated/capability_graph.json @@ -2201,7 +2201,7 @@ "evidence_state": "manual-baseline", "health": "challenger", "state": "experimental", - "version": "1.0.0", + "version": "1.0.1", "visibility": "advertised" }, "owner": { @@ -2212,10 +2212,10 @@ "package": { "files": [ { - "bytes": 5527, + "bytes": 5683, "executable": false, "path": "skills/aoa-session-memory-evidence-route/SKILL.md", - "sha256": "81156bda70d2a4ed2602765e1394010bba2de2f764e39c4c6ca2daa6fcd6ef74" + "sha256": "519e7c5acf57692be57a188bb227ad08c6441f02ae3e823684125de0c7c9b628" }, { "bytes": 6665, @@ -2236,8 +2236,8 @@ "sha256": "045fd6f1d7ef8458873700183a9838794ac5f8b6097b82f9d03da85838b6051e" } ], - "fingerprint": "383667122ea265f7ddd2387b7129339de7cee5f17d45082d9056ac264bd10b01", - "version": "1.0.0" + "fingerprint": "2adca52eea64742ad3f6625e903ac6cd521af2eac3229368d9bda5c3ac9760c0", + "version": "1.0.1" }, "primary_parent": "session-memory.use.query", "procedure": { @@ -2330,7 +2330,7 @@ "rationale": "It is the compact globally visible entrypoint and has a distinct routing output ABI." }, "contract_level": "executable", - "description": "Use in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.", + "description": "Use when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.", "execution": { "effects": [ "read-only" @@ -2376,7 +2376,7 @@ "evidence_state": "manual-baseline", "health": "challenger", "state": "experimental", - "version": "1.0.0", + "version": "1.0.1", "visibility": "advertised" }, "owner": { @@ -2387,14 +2387,14 @@ "package": { "files": [ { - "bytes": 4240, + "bytes": 5256, "executable": false, "path": "skills/aoa-session-memory-global-route/SKILL.md", - "sha256": "cf73f6b25c6f38afac17bc3a32be715936dc42b1b5738705d9d57d2d7647ac3b" + "sha256": "60af212b6f98d56cb0476b3479a37cddcfcce4d58de85d6231f352ceda74a688" } ], - "fingerprint": "f459356ad356a67927afa6ca4ce9d85e70848f3848b25d488e8fdb036ac39256", - "version": "1.0.0" + "fingerprint": "d0e36e745baafe440102252d8a3747d5e0b97678e63436934a24255d922490ee", + "version": "1.0.1" }, "primary_parent": "session-memory.use.route", "procedure": { @@ -12231,6 +12231,7 @@ { "contract_tokens": [ "1.0.0", + "1.0.1", "abi", "about", "access", @@ -12624,6 +12625,7 @@ "package_tokens": [ "about", "absence", + "across", "actually", "after", "agent", @@ -12676,6 +12678,7 @@ "benefit", "better", "between", + "bind", "blocks", "bodies", "boundary", @@ -12816,6 +12819,7 @@ "honest", "honestly", "hook", + "hooks", "host", "how", "id", @@ -12878,6 +12882,7 @@ "missing", "mm", "mode", + "models", "modes", "must", "mutating", @@ -12946,6 +12951,7 @@ "python3", "qualifies", "quality", + "queries", "query", "query-modes.md", "question", @@ -12954,6 +12960,7 @@ "read", "read-only", "readable", + "reads", "reasoning", "rebuild", "receipt", @@ -13280,14 +13287,16 @@ "сырой-ref", "сырым" ], - "search_text": "navigation\nRetrieve bounded source-linked evidence or rehydration context.\nsession-memory.use.query\nquery\nsearch\nevidence\nrehydrate\nзапрос\ncapability\nowner-verified\nhealthy\nactive\n1.0.0\ninternal\nauthored\naoa-session-memory\ncapabilities/families/session-memory.yaml\nsession-memory.use\nsession-memory\ncapabilities/families/session-memory.yaml\nQuery session memory\nEntity-anchored historical question.\nquestion\nprior-session-evidence-question\nOptional typed search packet from the search capability.\nsearch_evidence\nsearch-evidence-packet\nBounded evidence packet with freshness and source refs.\npacket\nevidence-packet\nThe user asks what a component does but does not ask how it behaved previously.\nCurrent repository or runtime state is the stronger source and no historical behavior question remains.\nThe answer depends on what happened in prior .aoa sessions and must carry resolvable raw or segment refs.\navailable\nskill\nroute-evidence\nskills/aoa-session-memory-evidence-route/SKILL.md\nindependent-callable\nretain\nIt has a distinct historical-evidence trigger, evidence-packet ABI, and proof-ceiling failure modes.\nexecutable\nUse when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nread-only\nTreating route counts as truth.\nInferring invocation from selection or a skill read.\nBroad raw scanning before typed routes.\nHiding stale or truncated providers.\nbounded-judgment\nnot-applicable\nStop when the bounded packet answers the question or exposes the exact next evidence expansion.\nread-only session-memory MCP or portable CLI\narchived evidence refs\nEvery important claim has a raw\nsegment\nsession\nreceipt\nor owner ref.\nFreshness and truncation are explicit.\nSkill evidence does not overstate invocation or effect.\nskill.aoa-session-memory-evidence-route\nprior-session\nevidence\nraw-ref\nsegment-ref\nusage-chain\nmcp\nskill-attribution\nдоказательство\nистория\nиспользование\nиспользовался\nсырой-ref\nсырым\nссылкам\nраньше\nпроизошло\nпоследствие\nskill\nmanual-baseline\nchallenger\nexperimental\n1.0.0\nadvertised\nauthored\naoa-session-memory\nskills/aoa-session-memory-evidence-route/SKILL.md\nsession-memory.use.query\nskills/aoa-session-memory-evidence-route/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-memory-evidence-route/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nReturn bounded, fresh, source-linked session evidence while keeping mention, selection, behavior, verification, and consequence distinct.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nPrior-session evidence route\nauthored-procedure\nSession id, label, or resolvable handle.\ntarget\nsession-target\nBounded continuation context and evidence refs.\npacket\nrehydration-packet\nThe user asks to remember generally without naming prior session scope.\nThe current active session already contains the required state or raw forensic review is explicitly required.\nA named archived session must be resumed or inspected without full raw loading.\navailable\nskill\nrehydrate\nskills/aoa-session-rehydrate/SKILL.md\nindependent-callable\nretain-deferred\nIt has a distinct continuation ABI but lacks current behavioral lift evidence.\nexecutable\nUse when an agent needs to resume, inspect, or continue from an archived `.aoa` session without loading the full raw transcript.\nread-only\nLoading full raw evidence prematurely.\nMixing multiple sessions.\nPresenting stale generated context as current runtime truth.\nbounded-judgment\nnot-applicable\nStop after sufficient bounded context or an exact unresolved-target handoff.\nsession-memory CLI\nread access to generated session surfaces\nThe target resolves uniquely.\nThe packet preserves source refs and open-thread boundaries.\nskill.aoa-session-rehydrate\nrehydrate\nresume\ncontinuation\nsession-target\nrehydrate-session\nвосстановление\nskill\nhypothesis\nchallenger\nexperimental\n1.0.0\ndeferred\nauthored\naoa-session-memory\nskills/aoa-session-rehydrate/SKILL.md\nsession-memory.use.query\nskills/aoa-session-rehydrate/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-rehydrate/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nRecover the smallest sufficient context from generated session surfaces and resolvable evidence refs.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nBounded session rehydration\nauthored-procedure\nQuery and bounded filters.\nquery\nsession-query\nOptional refreshed session projection.\nindexed_sessions\nindexed-session-set\nRanked results with freshness and evidence refs.\nresults\nsearch-evidence-packet\nThe request says search without identifying current source versus prior-session evidence.\nA current file\nowner API\nor typed entity route answers the question without archive search.\nA bounded archive query or explicit portable search-index refresh is required.\navailable\nskill\nquery-or-refresh\nskills/aoa-session-search/SKILL.md\nindependent-callable\nretain\nQuery and index-recovery share one stable search trigger; maintenance details remain internal modes and references.\nexecutable\nUse when an agent needs to build or query the portable `.aoa` SQLite search index for archived Codex sessions, with evidence refs and freshness checks.\nread-only\nruntime-mutation\nUsing search snippets as source authority.\nRefreshing or shrinking indexes without explicit intent.\nFalling through to broad text after a typed route succeeds.\nbounded-judgment\nreversible\nStop at a bounded result set\nhonest miss\nor explicit stale-provider recovery route.\npython3\nsession-memory CLI\nSQLite\nProvider freshness is explicit.\nResults preserve source refs and filters.\nAny refresh reports changed projection state.\nskill.aoa-session-search\nsearch\nsqlite\nfts\nprovider\nfreshness\nindex\nпоиск\nskill\nhypothesis\nchallenger\nexperimental\n1.0.0\ndeferred\nProvider status names the resulting freshness state.\nSearch results remain source-linked.\nRebuild the derived SQLite projection from preserved session indexes.\nKeep raw archives unchanged.\nauthored\naoa-session-memory\nskills/aoa-session-search/SKILL.md\nsession-memory.use.query\nskills/aoa-session-search/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-search/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nQuery or explicitly refresh the portable search projection without confusing index state with evidence truth.\nforbidden\nread-session-archives\nwrite-derived-session-index\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nPortable session search\nauthored-procedure\n---\nname: aoa-session-memory-evidence-route\ndescription: Use when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: evidence-router\n---\n\n# aoa-session-memory-evidence-route\n\n## Trigger Boundary\n\nUse this skill for a historical behavior question whose answer must come from\nprior session evidence. Do not use it to replace current repository source,\nlive runtime state, decision authority, eval verdicts, or durable promotion.\n\nThe route finds and qualifies evidence. It never upgrades mention, prompt\nvisibility, selection, loading, or a `SKILL.md` read into invocation or\neffectiveness.\n\n## Procedure\n\n1. Resolve the logical `` and the entity kind. Start with the\n smallest typed route:\n\n - source identity or “does it exist”: `entity-registry --lookup`;\n - behavior and nearby consequence: `usage-chain --kind `;\n - goal lifecycle: `goal-lifecycles`;\n - task-to-answer chain: `task-answer-chain`;\n - exact text, command, path, error, or session id: `literal-query-plan`;\n - relation between two entities: `graph-bridge`;\n - current route quality: `live-scenario-corpus check`.\n\n2. Prefer the read-only session-memory MCP equivalent when it is actually\n available. If transport or registry freshness blocks it, name the CLI\n fallback and run the same typed route from ``. A missing MCP tool\n is a transport/runtime issue, not permission for unbounded raw search.\n\n3. Read applicability before score. Inspect freshness, ambiguity, truncation,\n omitted counts, cost profile, and the exact next expansion before opening\n another layer.\n\n4. For a skill anchor, read `skill_evidence` explicitly:\n\n - `prompt_visible`, `selected`, `skill_read`, `edited`, `mentioned`,\n `cooccurrence`, and `deflected` are candidate states only;\n - `procedure_observed`, `verified`, and `completed` require an owner receipt\n or reviewed task episode;\n - `task_episode_refs` are join keys, not proof;\n - `invocation_claim_allowed=false` is a hard claim boundary;\n - foreign-correlation results remain auditable context, never consequence.\n\n5. For a comparative skill audit about successful, failed, or\n procedure-deviating applications:\n\n - start from `usage-chain --kind skill --full` and keep its\n `task_episode_refs` only as bounded join candidates;\n - for each selected candidate, run `task-answer-chain --session \n --task-episode-id --allow-missing-reasoning\n --allow-missing-answer` before attributing a result to the skill; the\n explicit flags preserve failed, interrupted, and receipt-bearing episodes\n that have no ordinary final answer;\n - route exact reviewed receipt, error, status, or failure-class anchors\n through `literal-query-plan` with the same session and episode scope, then\n open the proposed bounded evidence window;\n - call an application successful only when reviewed result or verification\n evidence belongs to the same procedure and correlation chain;\n - treat missing positive proof as `unresolved`, not as a failed application,\n and never turn a bounded set of failures into an archive-wide absence\n claim.\n\n6. Expand only the missing evidence class. Use raw or segment refs to verify an\n important claim, exact error, or bounded interval. Keep private bodies,\n hidden reasoning, and unrelated context outside the packet.\n\n7. Return the evidence packet to the source owner, decision owner,\n `aoa-evals`, or another named authority. Do not write that owner from\n session evidence alone.\n\nFor a specialized route, open exactly one shallow reference:\n\n- [consumer-profiles.md](references/consumer-profiles.md) for typed query\n selection and packet interpretation;\n- [maintenance-and-graph.md](references/maintenance-and-graph.md) for search\n pressure, graph pressure, rollups, and shrink/prune stop-lines;\n- [mcp-fallback.md](references/mcp-fallback.md) for exact MCP tools,\n preflight, and equivalent CLI commands.\n\n## Authority Boundary\n\nSession-memory packets are generated navigation and evidence surfaces. They\nare weaker than current owner source, repository decisions, canonical skills,\ncentral eval verdicts, and reviewed promotion surfaces. Route counts and\nsemantic proximity do not establish correctness, causality, invocation, or\nbenefit.\n\n## Verification\n\n- Name the first route and why it matched the question.\n- Name MCP or explicit CLI fallback.\n- Report freshness, ambiguity, truncation, and privacy posture.\n- Cite at least one resolvable raw, segment, session, receipt, or owner ref for\n each important historical claim.\n- For skill behavior, name exact package version/fingerprint when a reviewed\n receipt provides it and list alternative explanations that remain.\n- For candidate-semantics regression, run the reviewed\n `skill_candidate_semantics_contract` corpus case and label its synthetic\n evidence origin.\n\n## Stop Line\n\nStop when the bounded packet answers the question with resolvable refs, or\nwhen it exposes one exact next expansion and the claim remains honestly\nunresolved. Never compensate for a stale provider, closed transport,\nunavailable receipt, or missing authority by widening into an unbounded\ntranscript scan.\n\n---\nname: aoa-session-rehydrate\ndescription: Use when an agent needs to resume, inspect, or continue from an archived `.aoa` session without loading the full raw transcript.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: manual\n---\n\n# aoa-session-rehydrate\n\nUse when an agent needs to resume work from an archived session without loading\nthe full raw transcript.\n\n## Trigger Boundary\n\n- A user asks to resume, inspect, or continue a prior session.\n- Compaction has made current context unreliable.\n- A session archive exists under `.aoa/sessions/`.\n\n## Procedure\n\n1. Prefer the readable `.aoa/sessions/YYYY-MM-DD__NNN__short-title` directory,\n or read `.aoa/session-registry.json` when the target is ambiguous.\n2. Open the target session `SESSION.md`.\n3. Open `session.manifest.json`.\n4. Open the relevant segment `.index.json`.\n5. Load only the relevant segment events by `md_anchor` or `raw_ref`.\n6. Mark claims as provisional unless reviewed distillation exists.\n\n## Verification\n\n- The rehydration path names the exact session id.\n- The answer cites segment event ids or raw refs for important claims.\n- The agent does not rely on a summary when raw/index evidence is available.\n\n## Stop Line\n\nDo not read every raw event by default. Use the index as navigation.\n\n---\nname: aoa-session-search\ndescription: Use when an agent needs to build or query the portable `.aoa` SQLite search index for archived Codex sessions, with evidence refs and freshness checks.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: manual\n---\n\n# aoa-session-search\n\n## Trigger Boundary\n\nUse this skill for a bounded cross-session archive query or an explicitly\nrequested portable search-projection refresh. Do not use it when current owner\nsource, a typed entity route, or live runtime state already answers the\nquestion.\n\nSearch results are navigation. Raw, segment, session, receipt, and owner refs\nremain the evidence handoff.\n\n## Procedure\n\n1. Resolve logical `` and ``.\n2. Inspect provider status and freshness before choosing query or maintenance.\n3. Prefer the smallest typed query and bounded filters:\n\n```bash\npython3 scripts/aoa_session_memory.py search \\\n --workspace-root \\\n --aoa-root \\\n --query \"\" \\\n --explain\n```\n\n4. Use dedicated agent-event, entity, goal, or task routes when their typed\n contract fits better than general search. Keep literal raw-text timeout\n bounded; an explicit offline scan is a separate operator choice.\n5. Open returned raw or segment refs before using a hit for a decision, name,\n distillation, automation candidate, or other owner mutation.\n6. Rebuild or repair a projection only after explicit intent and a preview or\n gate packet. Query and maintenance share this package, but maintenance is a\n typed internal mode, not an automatic consequence of a stale hit.\n\nUse exactly one shallow reference when needed:\n\n- [query-modes.md](references/query-modes.md) for filters, shards, host\n overlays, dedicated response routes, and retrieval packets;\n- [maintenance.md](references/maintenance.md) for index builds, dirty\n catch-up, entity sync, shrink gates, guarded apply, and recovery.\n\n## Verification\n\n- Every hit names `session_label`, an event or segment identity, resolvable\n evidence refs, and freshness.\n- Treat only `fresh` hits as current routing evidence. Reindex or inspect raw\n when stale or unverifiable.\n- Record provider warnings without replacing portable SQLite authority.\n- A mutating mode reports preview/gates, exact scope, changed derived\n artifacts, postcondition status, and recovery route.\n- Raw archives and segment evidence remain unchanged by search maintenance.\n\n## Stop Line\n\nStop after a bounded result set, honest miss, or one exact stale-provider\nrecovery route. Do not widen from a successful typed route into broad FTS, run\nmaintenance implicitly, claim storage improvement without before/after\nevidence, or treat a search hit as reviewed truth.\n", + "search_text": "navigation\nRetrieve bounded source-linked evidence or rehydration context.\nsession-memory.use.query\nquery\nsearch\nevidence\nrehydrate\nзапрос\ncapability\nowner-verified\nhealthy\nactive\n1.0.0\ninternal\nauthored\naoa-session-memory\ncapabilities/families/session-memory.yaml\nsession-memory.use\nsession-memory\ncapabilities/families/session-memory.yaml\nQuery session memory\nEntity-anchored historical question.\nquestion\nprior-session-evidence-question\nOptional typed search packet from the search capability.\nsearch_evidence\nsearch-evidence-packet\nBounded evidence packet with freshness and source refs.\npacket\nevidence-packet\nThe user asks what a component does but does not ask how it behaved previously.\nCurrent repository or runtime state is the stronger source and no historical behavior question remains.\nThe answer depends on what happened in prior .aoa sessions and must carry resolvable raw or segment refs.\navailable\nskill\nroute-evidence\nskills/aoa-session-memory-evidence-route/SKILL.md\nindependent-callable\nretain\nIt has a distinct historical-evidence trigger, evidence-packet ABI, and proof-ceiling failure modes.\nexecutable\nUse when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nread-only\nTreating route counts as truth.\nInferring invocation from selection or a skill read.\nBroad raw scanning before typed routes.\nHiding stale or truncated providers.\nbounded-judgment\nnot-applicable\nStop when the bounded packet answers the question or exposes the exact next evidence expansion.\nread-only session-memory MCP or portable CLI\narchived evidence refs\nEvery important claim has a raw\nsegment\nsession\nreceipt\nor owner ref.\nFreshness and truncation are explicit.\nSkill evidence does not overstate invocation or effect.\nskill.aoa-session-memory-evidence-route\nprior-session\nevidence\nraw-ref\nsegment-ref\nusage-chain\nmcp\nskill-attribution\nдоказательство\nистория\nиспользование\nиспользовался\nсырой-ref\nсырым\nссылкам\nраньше\nпроизошло\nпоследствие\nskill\nmanual-baseline\nchallenger\nexperimental\n1.0.1\nadvertised\nauthored\naoa-session-memory\nskills/aoa-session-memory-evidence-route/SKILL.md\nsession-memory.use.query\nskills/aoa-session-memory-evidence-route/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-memory-evidence-route/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nReturn bounded, fresh, source-linked session evidence while keeping mention, selection, behavior, verification, and consequence distinct.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nPrior-session evidence route\nauthored-procedure\nSession id, label, or resolvable handle.\ntarget\nsession-target\nBounded continuation context and evidence refs.\npacket\nrehydration-packet\nThe user asks to remember generally without naming prior session scope.\nThe current active session already contains the required state or raw forensic review is explicitly required.\nA named archived session must be resumed or inspected without full raw loading.\navailable\nskill\nrehydrate\nskills/aoa-session-rehydrate/SKILL.md\nindependent-callable\nretain-deferred\nIt has a distinct continuation ABI but lacks current behavioral lift evidence.\nexecutable\nUse when an agent needs to resume, inspect, or continue from an archived `.aoa` session without loading the full raw transcript.\nread-only\nLoading full raw evidence prematurely.\nMixing multiple sessions.\nPresenting stale generated context as current runtime truth.\nbounded-judgment\nnot-applicable\nStop after sufficient bounded context or an exact unresolved-target handoff.\nsession-memory CLI\nread access to generated session surfaces\nThe target resolves uniquely.\nThe packet preserves source refs and open-thread boundaries.\nskill.aoa-session-rehydrate\nrehydrate\nresume\ncontinuation\nsession-target\nrehydrate-session\nвосстановление\nskill\nhypothesis\nchallenger\nexperimental\n1.0.0\ndeferred\nauthored\naoa-session-memory\nskills/aoa-session-rehydrate/SKILL.md\nsession-memory.use.query\nskills/aoa-session-rehydrate/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-rehydrate/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nRecover the smallest sufficient context from generated session surfaces and resolvable evidence refs.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nBounded session rehydration\nauthored-procedure\nQuery and bounded filters.\nquery\nsession-query\nOptional refreshed session projection.\nindexed_sessions\nindexed-session-set\nRanked results with freshness and evidence refs.\nresults\nsearch-evidence-packet\nThe request says search without identifying current source versus prior-session evidence.\nA current file\nowner API\nor typed entity route answers the question without archive search.\nA bounded archive query or explicit portable search-index refresh is required.\navailable\nskill\nquery-or-refresh\nskills/aoa-session-search/SKILL.md\nindependent-callable\nretain\nQuery and index-recovery share one stable search trigger; maintenance details remain internal modes and references.\nexecutable\nUse when an agent needs to build or query the portable `.aoa` SQLite search index for archived Codex sessions, with evidence refs and freshness checks.\nread-only\nruntime-mutation\nUsing search snippets as source authority.\nRefreshing or shrinking indexes without explicit intent.\nFalling through to broad text after a typed route succeeds.\nbounded-judgment\nreversible\nStop at a bounded result set\nhonest miss\nor explicit stale-provider recovery route.\npython3\nsession-memory CLI\nSQLite\nProvider freshness is explicit.\nResults preserve source refs and filters.\nAny refresh reports changed projection state.\nskill.aoa-session-search\nsearch\nsqlite\nfts\nprovider\nfreshness\nindex\nпоиск\nskill\nhypothesis\nchallenger\nexperimental\n1.0.0\ndeferred\nProvider status names the resulting freshness state.\nSearch results remain source-linked.\nRebuild the derived SQLite projection from preserved session indexes.\nKeep raw archives unchanged.\nauthored\naoa-session-memory\nskills/aoa-session-search/SKILL.md\nsession-memory.use.query\nskills/aoa-session-search/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-search/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nQuery or explicitly refresh the portable search projection without confusing index state with evidence truth.\nforbidden\nread-session-archives\nwrite-derived-session-index\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nPortable session search\nauthored-procedure\n---\nname: aoa-session-memory-evidence-route\ndescription: Use when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: evidence-router\n---\n\n# aoa-session-memory-evidence-route\n\n## Trigger Boundary\n\nUse this skill for a historical behavior question whose answer must come from\nprior session evidence. Do not use it to replace current repository source,\nlive runtime state, decision authority, eval verdicts, or durable promotion.\n\nThe route finds and qualifies evidence. It never upgrades mention, prompt\nvisibility, selection, loading, or a `SKILL.md` read into invocation or\neffectiveness.\n\nBind bounded queries through the available MCP or owner CLI. Archive reads\npreserve the same evidence contract across models and do not verify live hooks.\n\n## Procedure\n\n1. Resolve the logical `` and the entity kind. Start with the\n smallest typed route:\n\n - source identity or “does it exist”: `entity-registry --lookup`;\n - behavior and nearby consequence: `usage-chain --kind `;\n - goal lifecycle: `goal-lifecycles`;\n - task-to-answer chain: `task-answer-chain`;\n - exact text, command, path, error, or session id: `literal-query-plan`;\n - relation between two entities: `graph-bridge`;\n - current route quality: `live-scenario-corpus check`.\n\n2. Prefer the read-only session-memory MCP equivalent when it is actually\n available. If transport or registry freshness blocks it, name the CLI\n fallback and run the same typed route from ``. A missing MCP tool\n is a transport/runtime issue, not permission for unbounded raw search.\n\n3. Read applicability before score. Inspect freshness, ambiguity, truncation,\n omitted counts, cost profile, and the exact next expansion before opening\n another layer.\n\n4. For a skill anchor, read `skill_evidence` explicitly:\n\n - `prompt_visible`, `selected`, `skill_read`, `edited`, `mentioned`,\n `cooccurrence`, and `deflected` are candidate states only;\n - `procedure_observed`, `verified`, and `completed` require an owner receipt\n or reviewed task episode;\n - `task_episode_refs` are join keys, not proof;\n - `invocation_claim_allowed=false` is a hard claim boundary;\n - foreign-correlation results remain auditable context, never consequence.\n\n5. For a comparative skill audit about successful, failed, or\n procedure-deviating applications:\n\n - start from `usage-chain --kind skill --full` and keep its\n `task_episode_refs` only as bounded join candidates;\n - for each selected candidate, run `task-answer-chain --session \n --task-episode-id --allow-missing-reasoning\n --allow-missing-answer` before attributing a result to the skill; the\n explicit flags preserve failed, interrupted, and receipt-bearing episodes\n that have no ordinary final answer;\n - route exact reviewed receipt, error, status, or failure-class anchors\n through `literal-query-plan` with the same session and episode scope, then\n open the proposed bounded evidence window;\n - call an application successful only when reviewed result or verification\n evidence belongs to the same procedure and correlation chain;\n - treat missing positive proof as `unresolved`, not as a failed application,\n and never turn a bounded set of failures into an archive-wide absence\n claim.\n\n6. Expand only the missing evidence class. Use raw or segment refs to verify an\n important claim, exact error, or bounded interval. Keep private bodies,\n hidden reasoning, and unrelated context outside the packet.\n\n7. Return the evidence packet to the source owner, decision owner,\n `aoa-evals`, or another named authority. Do not write that owner from\n session evidence alone.\n\nFor a specialized route, open exactly one shallow reference:\n\n- [consumer-profiles.md](references/consumer-profiles.md) for typed query\n selection and packet interpretation;\n- [maintenance-and-graph.md](references/maintenance-and-graph.md) for search\n pressure, graph pressure, rollups, and shrink/prune stop-lines;\n- [mcp-fallback.md](references/mcp-fallback.md) for exact MCP tools,\n preflight, and equivalent CLI commands.\n\n## Authority Boundary\n\nSession-memory packets are generated navigation and evidence surfaces. They\nare weaker than current owner source, repository decisions, canonical skills,\ncentral eval verdicts, and reviewed promotion surfaces. Route counts and\nsemantic proximity do not establish correctness, causality, invocation, or\nbenefit.\n\n## Verification\n\n- Name the first route and why it matched the question.\n- Name MCP or explicit CLI fallback.\n- Report freshness, ambiguity, truncation, and privacy posture.\n- Cite at least one resolvable raw, segment, session, receipt, or owner ref for\n each important historical claim.\n- For skill behavior, name exact package version/fingerprint when a reviewed\n receipt provides it and list alternative explanations that remain.\n- For candidate-semantics regression, run the reviewed\n `skill_candidate_semantics_contract` corpus case and label its synthetic\n evidence origin.\n\n## Stop Line\n\nStop when the bounded packet answers the question with resolvable refs, or\nwhen it exposes one exact next expansion and the claim remains honestly\nunresolved. Never compensate for a stale provider, closed transport,\nunavailable receipt, or missing authority by widening into an unbounded\ntranscript scan.\n\n---\nname: aoa-session-rehydrate\ndescription: Use when an agent needs to resume, inspect, or continue from an archived `.aoa` session without loading the full raw transcript.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: manual\n---\n\n# aoa-session-rehydrate\n\nUse when an agent needs to resume work from an archived session without loading\nthe full raw transcript.\n\n## Trigger Boundary\n\n- A user asks to resume, inspect, or continue a prior session.\n- Compaction has made current context unreliable.\n- A session archive exists under `.aoa/sessions/`.\n\n## Procedure\n\n1. Prefer the readable `.aoa/sessions/YYYY-MM-DD__NNN__short-title` directory,\n or read `.aoa/session-registry.json` when the target is ambiguous.\n2. Open the target session `SESSION.md`.\n3. Open `session.manifest.json`.\n4. Open the relevant segment `.index.json`.\n5. Load only the relevant segment events by `md_anchor` or `raw_ref`.\n6. Mark claims as provisional unless reviewed distillation exists.\n\n## Verification\n\n- The rehydration path names the exact session id.\n- The answer cites segment event ids or raw refs for important claims.\n- The agent does not rely on a summary when raw/index evidence is available.\n\n## Stop Line\n\nDo not read every raw event by default. Use the index as navigation.\n\n---\nname: aoa-session-search\ndescription: Use when an agent needs to build or query the portable `.aoa` SQLite search index for archived Codex sessions, with evidence refs and freshness checks.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: manual\n---\n\n# aoa-session-search\n\n## Trigger Boundary\n\nUse this skill for a bounded cross-session archive query or an explicitly\nrequested portable search-projection refresh. Do not use it when current owner\nsource, a typed entity route, or live runtime state already answers the\nquestion.\n\nSearch results are navigation. Raw, segment, session, receipt, and owner refs\nremain the evidence handoff.\n\n## Procedure\n\n1. Resolve logical `` and ``.\n2. Inspect provider status and freshness before choosing query or maintenance.\n3. Prefer the smallest typed query and bounded filters:\n\n```bash\npython3 scripts/aoa_session_memory.py search \\\n --workspace-root \\\n --aoa-root \\\n --query \"\" \\\n --explain\n```\n\n4. Use dedicated agent-event, entity, goal, or task routes when their typed\n contract fits better than general search. Keep literal raw-text timeout\n bounded; an explicit offline scan is a separate operator choice.\n5. Open returned raw or segment refs before using a hit for a decision, name,\n distillation, automation candidate, or other owner mutation.\n6. Rebuild or repair a projection only after explicit intent and a preview or\n gate packet. Query and maintenance share this package, but maintenance is a\n typed internal mode, not an automatic consequence of a stale hit.\n\nUse exactly one shallow reference when needed:\n\n- [query-modes.md](references/query-modes.md) for filters, shards, host\n overlays, dedicated response routes, and retrieval packets;\n- [maintenance.md](references/maintenance.md) for index builds, dirty\n catch-up, entity sync, shrink gates, guarded apply, and recovery.\n\n## Verification\n\n- Every hit names `session_label`, an event or segment identity, resolvable\n evidence refs, and freshness.\n- Treat only `fresh` hits as current routing evidence. Reindex or inspect raw\n when stale or unverifiable.\n- Record provider warnings without replacing portable SQLite authority.\n- A mutating mode reports preview/gates, exact scope, changed derived\n artifacts, postcondition status, and recovery route.\n- Raw archives and segment evidence remain unchanged by search maintenance.\n\n## Stop Line\n\nStop after a bounded result set, honest miss, or one exact stale-provider\nrecovery route. Do not widen from a successful typed route into broad FTS, run\nmaintenance implicitly, claim storage improvement without before/after\nevidence, or treat a search hit as reviewed truth.\n", "title": "Query session memory", "tokens": [ "1.0.0", + "1.0.1", "abi", "about", "absence", "access", + "across", "active", "actually", "adapter", @@ -13355,6 +13364,7 @@ "benefit", "better", "between", + "bind", "blocks", "bodies", "boundaries", @@ -13529,6 +13539,7 @@ "honest", "honestly", "hook", + "hooks", "host", "how", "hypothesis", @@ -13610,6 +13621,7 @@ "mixing", "mm", "mode", + "models", "modes", "multiple", "must", @@ -13695,6 +13707,7 @@ "python3", "qualifies", "quality", + "queries", "query", "query-modes.md", "query-or-refresh", @@ -13708,6 +13721,7 @@ "read-owner-source", "read-session-archives", "readable", + "reads", "reasoning", "rebuild", "receipt", @@ -13946,6 +13960,7 @@ { "contract_tokens": [ "1.0.0", + "1.0.1", "abi", "access", "active", @@ -13953,6 +13968,7 @@ "adapter-specific", "advertised", "after", + "agent", "alone", "already", "anchor", @@ -13962,6 +13978,7 @@ "aoa-session-memory-global-route", "applicability", "applicable", + "archive", "authored", "authored-procedure", "authority", @@ -13978,7 +13995,6 @@ "capability-home-v1", "capability-selection", "challenger", - "codex", "command", "compact", "compaction", @@ -13993,7 +14009,6 @@ "exhausted", "experimental", "explicit", - "failures", "families", "filesystem", "fit", @@ -14018,6 +14033,7 @@ "known", "line", "logical", + "lookup", "manual", "manual-baseline", "match", @@ -14028,6 +14044,7 @@ "narrower", "narrowest", "navigation", + "needs", "negative", "no", "no-fit", @@ -14044,8 +14061,8 @@ "owner-verified", "package", "preserve", - "prior", "procedure", + "projection", "prompt", "prompt-visible", "proof", @@ -14060,6 +14077,7 @@ "resolve", "result", "retain", + "retrieval", "roots", "route", "router", @@ -14135,18 +14153,24 @@ "session-memory" ], "package_tokens": [ + "acceptance", "access", "acting", "action", + "activate", "active", + "actual", + "actually", + "adapter", + "adapter-specific", "adapters", "adapters.codex", "admitted", "against", + "agent", "agents", "agents.md", "alone", - "any", "aoa", "aoa-root", "aoa-search", @@ -14154,24 +14178,34 @@ "aoa-session-memory-global-route", "apache", "apache-2.0", + "app", + "app-server", "applicability", "applicable", "architecture", "archive", + "archived", "asks", "assure", "audit", + "authority", + "authorized", + "availability", "available", "batch", "before", "behavior", + "bind", + "binding", "bindings", + "block", "both", "boundaries", "boundary", "branches", "bundle", "candidate", + "capabilities", "capability", "capability-router.md", "capture", @@ -14182,41 +14216,52 @@ "chosen", "classifier", "classify", + "cli", "codex", "codex-compact-probe", "codex-history-root", "codex-hooks-status", - "commands", + "command", "compact", "compaction", "composition", "concrete", + "configuration", + "consumer", "context", "continuation", + "continuity", + "contract", "corpus", "curate", "current", "dag", "decision", + "declares", "deeper", "description", "design", "design.agents.md", "design.md", "diagnostics", + "different", "discovery", "distillation", "do", "doctor", "does", "effects", + "eligibility", "eval", "event", + "every", "evidence", "exact", + "execution", "explicitly", "export", - "failures", + "exposure", + "filesystem", "fingerprint", "first", "first-wave", @@ -14227,18 +14272,24 @@ "global", "global-router", "graph", + "guidance", "hand", "hand-writing", "hash", "historical", "history", + "home", + "home-port", "hook", "hooks", "host", "identity", "if", + "imply", "import", "incidents", + "including", + "independent", "index", "index.md", "indexes", @@ -14253,12 +14304,12 @@ "invocation", "its", "itself", - "jsonl", "keep", "known", "layers", "level", "license", + "lifecycle", "line", "live", "live-scenario-audit", @@ -14273,13 +14324,14 @@ "matches", "material", "matters", + "mcp", "md", "memory", "memory.py", - "mentions", "metadata", "missing", "mode", + "model", "more", "name", "names", @@ -14288,13 +14340,16 @@ "naming-readiness", "naming-wave", "naming.md", + "needs", "negative", "not", "one", "only", "open", + "operation", "operations", "optional", + "others", "owner", "owning", "package", @@ -14309,6 +14364,7 @@ "pipeline.md", "planner", "plans", + "port", "portability", "portable", "portable-source-root", @@ -14317,11 +14373,13 @@ "posture", "precompact", "preparing", + "preserved", "prior", "prior-session", "probe", "procedure", "project", + "projection", "prompt", "prompt-visible", "proof", @@ -14332,8 +14390,11 @@ "queues", "raw", "read", + "read-only", + "readable", "readiness", "readiness.md", + "reasoning", "receipt", "recipes", "references", @@ -14345,13 +14406,16 @@ "repair", "replace", "required", + "requirements", "resolve", + "response", "resume", "retrieval", "retrieval-packet", "retrieve", "review", "review-phase-name", + "role", "root", "roots", "route", @@ -14359,8 +14423,10 @@ "router", "routers", "routing", + "rule", "run", "runtime", + "same", "scenario", "scope", "scripts", @@ -14374,16 +14440,19 @@ "selection", "semantic", "separate", + "server", "session", "session-memory", "session-name", "sessions", "sessionstart", "set", + "settings", "similarity", "skill", "smallest", "source", + "specific", "sqlite", "stable", "stale", @@ -14403,7 +14472,9 @@ "taxonomy", "than", "then", + "they", "three", + "tool", "top", "top-level", "topical", @@ -14436,28 +14507,33 @@ "workspace-root", "writing" ], - "positive_text": "session-memory.use.route\nRoute session-memory work\nSelect the smallest applicable session-memory capability without widening prompt-visible context.\nroute\nrouter\napplicability\nмаршрутизация\nskill.aoa-session-memory-global-route\nGlobal session-memory router\nUse in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.\naoa\nsession-memory\ntranscripts\ncompaction\nrehydration\nhooks\nrouter\nпамять\nсессия\nархив\nсжатие\nмаршрутизация\nThe request names session memory\n.aoa\ntranscripts\ncompaction\nrehydration\nhooks\nor session-memory validation.", + "positive_text": "session-memory.use.route\nRoute session-memory work\nSelect the smallest applicable session-memory capability without widening prompt-visible context.\nroute\nrouter\napplicability\nмаршрутизация\nskill.aoa-session-memory-global-route\nGlobal session-memory router\nUse when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.\naoa\nsession-memory\ntranscripts\ncompaction\nrehydration\nhooks\nrouter\nпамять\nсессия\nархив\nсжатие\nмаршрутизация\nThe request names session memory\n.aoa\ntranscripts\ncompaction\nrehydration\nhooks\nor session-memory validation.", "positive_tokens": [ - "any", + "adapter", + "adapter-specific", + "agent", "aoa", "applicability", "applicable", + "archive", "capability", - "codex", "compaction", "context", - "failures", + "evidence", "global", "hook", "hooks", + "lookup", "memory", - "mentions", "names", - "prior", + "needs", + "operation", + "projection", "prompt", "prompt-visible", "rehydration", "request", + "retrieval", "route", "router", "select", @@ -14467,9 +14543,10 @@ "skill", "skill.aoa-session-memory-global-route", "smallest", + "specific", + "transcript", "transcripts", "use", - "user", "validation", "visible", "when", @@ -14500,15 +14577,20 @@ "сесс", "сжат" ], - "search_text": "navigation\nSelect the smallest applicable session-memory capability without widening prompt-visible context.\nsession-memory.use.route\nroute\nrouter\napplicability\nмаршрутизация\ncapability\nowner-verified\nhealthy\nactive\n1.0.0\ninternal\nauthored\naoa-session-memory\ncapabilities/families/session-memory.yaml\nsession-memory.use\nsession-memory\ncapabilities/families/session-memory.yaml\nRoute session-memory work\nOne user request requiring owner routing.\nrequest\nsession-memory-request\nOne bounded capability selection or explicit handoff.\nselection\ncapability-selection\nThe request mentions memory generically without a session\ntranscript\nor .aoa anchor.\nA narrower non-session-memory owner is already known and no session evidence or session-memory operation is required.\nThe request names session memory\n.aoa\ntranscripts\ncompaction\nrehydration\nhooks\nor session-memory validation.\navailable\nskill\nroute\nskills/aoa-session-memory-global-route/SKILL.md\nindependent-callable\nretain\nIt is the compact globally visible entrypoint and has a distinct routing output ABI.\nexecutable\nUse in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.\nread-only\nSelecting from topical similarity alone.\nTreating generated routing output as procedure authority.\nWidening to raw evidence before a typed route is exhausted.\nbounded-judgment\nnot-applicable\nStop after one bounded selection\nhandoff\nor honest no-fit result.\nfilesystem read access\nsession-memory capability router\nThe selected package trigger and negative applicability match the request.\nThe selected owner and effects are stated before execution.\nskill.aoa-session-memory-global-route\naoa\nsession-memory\ntranscripts\ncompaction\nrehydration\nhooks\nrouter\nпамять\nсессия\nархив\nсжатие\nмаршрутизация\nskill\nmanual-baseline\nchallenger\nexperimental\n1.0.0\nadvertised\nauthored\naoa-session-memory\nskills/aoa-session-memory-global-route/SKILL.md\nsession-memory.use.route\nskills/aoa-session-memory-global-route/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-memory-global-route/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nSelect the narrowest owner capability and preserve source, runtime, and proof boundaries.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nGlobal session-memory router\nauthored-procedure\n---\nname: aoa-session-memory-global-route\ndescription: Use in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: global-router\n---\n\n# aoa-session-memory-global-route\n\nUse this as the top-level router for the AoA session-memory bundle.\n\n## Source Root\n\nResolve three logical roots before acting:\n\n- ``: the selected project workspace;\n- ``: the active installed session-memory root;\n- ``: the standalone owner source when source/export\n work is required.\n\nConcrete paths are runtime bindings, not portable skill identity.\n\n## Trigger Boundary\n\nUse this skill in any Codex session when the task touches:\n\n- `.aoa` session memory\n- Codex raw transcript JSONL\n- context compaction or compaction intervals\n- prior-session resume, rehydration, or session archive lookup\n- AoA hooks for `SessionStart`, `UserPromptSubmit`, `PreCompact`,\n `PostCompact`, or `Stop`\n- `raw_unavailable` incidents\n- `stress-pass`, `audit`, `doctor`, `codex-hooks-status`, or\n `codex-compact-probe`\n- event taxonomy, classifier, generated segment indexes, or `reindex-sessions`\n- portable SQLite search, `search-index`, `aoa-search`,\n `search-provider-status`, optional host provider gates, or retrieval\n freshness\n- retrieval packets, `retrieve`, `retrieval-packet`, or long-session\n continuation recipes\n- live route-quality checks, `live-scenario-audit`, or\n `live-scenario-corpus`\n- naming readiness, `SESSION_NAMES.md`, `sessions/INDEX.md`, or\n `naming-readiness`\n- mass session naming, `naming-wave`, semantic session-name review plans, or\n naming quality audit\n- phase discovery, `phase-discovery`, `review-phase-name`, or session naming\n candidate layers\n- historical Codex session import from ``\n- first-wave batch distillation or historical session review queues\n- preparing or validating the portable `aoa-session-memory` bundle\n\n## Procedure\n\n1. Read `/AGENTS.md` and classify the task before loading\n deeper context.\n2. Read only the owning surface: `DESIGN.md` for architecture or boundaries,\n `DESIGN.AGENTS.md` for query/access behavior, the relevant `PIPELINE.md`\n section for operations, `INSTALL.md` for portability, `NAMING.md` for names,\n or `READINESS.md` for proof posture.\n3. Read\n [references/capability-router.md](references/capability-router.md), verify\n its source hash against the generated graph when composition matters, and\n choose the smallest applicable bundle.\n4. Use `/scripts/aoa_session_memory.py` for commands.\n5. Keep historical raw/session material intact unless the user explicitly asks\n for a repair.\n6. If the task changes portable behavior, export to\n `` and validate both source and\n standalone surfaces.\n7. If the user-level router itself is missing or stale, run\n `install-user-skill` from the active install root instead of hand-writing a\n symlink.\n\n## Skill Routing\n\nUse the generated router card for exact package names, positive and negative\napplicability, visibility, version, and fingerprint. Its stable branches are:\n\n- `use.route` for top-level selection;\n- `use.query` for evidence, search, and rehydration;\n- `stewardship.capture`, `.project`, `.curate`, `.name`, and `.assure`;\n- `adapters.codex` for Codex hooks, transcripts, and compaction.\n\nFor more than one capability, use the full generated graph and task-local DAG\nplanner. Do not select a set from topical similarity alone.\n\n## Verification\n\n- The chosen capability's positive trigger matches and its negative trigger\n does not.\n- The selected package version/fingerprint matches the current generated\n router and installed runtime receipt when available.\n- Required inputs, permissions, effects, and verifier are known before action.\n- The prompt-visible set remains the two admitted routers unless a separate\n routing eval and owner decision changes it.\n\n## Stop Line\n\nDo not replace raw evidence with summaries. Use indexes and diagnostics first,\nthen open raw only for exact verification or repair.\n", + "search_text": "navigation\nSelect the smallest applicable session-memory capability without widening prompt-visible context.\nsession-memory.use.route\nroute\nrouter\napplicability\nмаршрутизация\ncapability\nowner-verified\nhealthy\nactive\n1.0.0\ninternal\nauthored\naoa-session-memory\ncapabilities/families/session-memory.yaml\nsession-memory.use\nsession-memory\ncapabilities/families/session-memory.yaml\nRoute session-memory work\nOne user request requiring owner routing.\nrequest\nsession-memory-request\nOne bounded capability selection or explicit handoff.\nselection\ncapability-selection\nThe request mentions memory generically without a session\ntranscript\nor .aoa anchor.\nA narrower non-session-memory owner is already known and no session evidence or session-memory operation is required.\nThe request names session memory\n.aoa\ntranscripts\ncompaction\nrehydration\nhooks\nor session-memory validation.\navailable\nskill\nroute\nskills/aoa-session-memory-global-route/SKILL.md\nindependent-callable\nretain\nIt is the compact globally visible entrypoint and has a distinct routing output ABI.\nexecutable\nUse when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.\nread-only\nSelecting from topical similarity alone.\nTreating generated routing output as procedure authority.\nWidening to raw evidence before a typed route is exhausted.\nbounded-judgment\nnot-applicable\nStop after one bounded selection\nhandoff\nor honest no-fit result.\nfilesystem read access\nsession-memory capability router\nThe selected package trigger and negative applicability match the request.\nThe selected owner and effects are stated before execution.\nskill.aoa-session-memory-global-route\naoa\nsession-memory\ntranscripts\ncompaction\nrehydration\nhooks\nrouter\nпамять\nсессия\nархив\nсжатие\nмаршрутизация\nskill\nmanual-baseline\nchallenger\nexperimental\n1.0.1\nadvertised\nauthored\naoa-session-memory\nskills/aoa-session-memory-global-route/SKILL.md\nsession-memory.use.route\nskills/aoa-session-memory-global-route/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-memory-global-route/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nSelect the narrowest owner capability and preserve source, runtime, and proof boundaries.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nGlobal session-memory router\nauthored-procedure\n---\nname: aoa-session-memory-global-route\ndescription: \"Use when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.\"\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: global-router\n---\n\n# aoa-session-memory-global-route\n\nUse this as the top-level router for the AoA session-memory bundle.\n\n## Source Root\n\nResolve three logical roots before acting:\n\n- ``: the selected project workspace;\n- ``: the active installed session-memory root;\n- ``: the standalone owner source when source/export\n work is required.\n\nConcrete paths are runtime bindings, not portable skill identity.\n\n## Required Capabilities\n\nSelection needs readable owner guidance and the capability router. Execution\nneeds the selected operation's actual inputs, permissions, and an available\nread-only MCP or owner CLI binding. A filesystem read, a query response, and\nan adapter lifecycle event are different capabilities; availability of one\ndoes not imply the others.\n\nUse the same evidence and scope contract for every consumer. Model and\nreasoning settings are execution configuration, not role identity, authority,\nor a different acceptance rule. The home-port's consumer exposure declares\neligibility only; it does not install or activate a runtime.\n\n## Trigger Boundary\n\nUse this skill when the task touches:\n\n- `.aoa` session memory\n- preserved session evidence or raw transcripts\n- context continuity or archived compaction intervals\n- prior-session resume, rehydration, or session archive lookup\n- capture hooks, including the Codex adapter's `SessionStart`,\n `UserPromptSubmit`, `PreCompact`, `PostCompact`, or `Stop`\n- `raw_unavailable` incidents\n- `stress-pass`, `audit`, `doctor`, `codex-hooks-status`, or\n `codex-compact-probe`\n- event taxonomy, classifier, generated segment indexes, or `reindex-sessions`\n- portable SQLite search, `search-index`, `aoa-search`,\n `search-provider-status`, optional host provider gates, or retrieval\n freshness\n- retrieval packets, `retrieve`, `retrieval-packet`, or long-session\n continuation recipes\n- live route-quality checks, `live-scenario-audit`, or\n `live-scenario-corpus`\n- naming readiness, `SESSION_NAMES.md`, `sessions/INDEX.md`, or\n `naming-readiness`\n- mass session naming, `naming-wave`, semantic session-name review plans, or\n naming quality audit\n- phase discovery, `phase-discovery`, `review-phase-name`, or session naming\n candidate layers\n- historical Codex session import from ``\n- first-wave batch distillation or historical session review queues\n- preparing or validating the portable `aoa-session-memory` bundle\n\n## Procedure\n\n1. Read `/AGENTS.md` and classify the task before loading\n deeper context.\n2. Read only the owning surface: `DESIGN.md` for architecture or boundaries,\n `DESIGN.AGENTS.md` for query/access behavior, the relevant `PIPELINE.md`\n section for operations, `INSTALL.md` for portability, `NAMING.md` for names,\n or `READINESS.md` for proof posture.\n3. Read\n [references/capability-router.md](references/capability-router.md), verify\n its source hash against the generated graph when composition matters, and\n choose the smallest applicable bundle.\n4. Bind the selected operation to an actually available read-only MCP tool or\n `/scripts/aoa_session_memory.py` CLI command. For Codex hooks,\n transcript import, app-server or live compaction, select `adapters.codex`\n and its exact requirements. Missing adapter capabilities block that\n operation; they do not block an independent authorized archive read.\n5. Keep historical raw/session material intact unless the user explicitly asks\n for a repair.\n6. If the task changes portable behavior, export to\n `` and validate both source and\n standalone surfaces.\n7. If the user-level router itself is missing or stale, run\n `install-user-skill` from the active install root instead of hand-writing a\n symlink.\n\n## Skill Routing\n\nUse the generated router card for exact package names, positive and negative\napplicability, visibility, version, and fingerprint. Its stable branches are:\n\n- `use.route` for top-level selection;\n- `use.query` for evidence, search, and rehydration;\n- `stewardship.capture`, `.project`, `.curate`, `.name`, and `.assure`;\n- `adapters.codex` for Codex hooks, transcripts, and compaction.\n\nFor more than one capability, use the full generated graph and task-local DAG\nplanner. Do not select a set from topical similarity alone.\n\n## Verification\n\n- The chosen capability's positive trigger matches and its negative trigger\n does not.\n- The selected package version/fingerprint matches the current generated\n router and installed runtime receipt when available.\n- Required inputs, permissions, effects, and verifier are known before action.\n- The prompt-visible set remains the two admitted routers unless a separate\n routing eval and owner decision changes it.\n\n## Stop Line\n\nDo not replace raw evidence with summaries. Use indexes and diagnostics first,\nthen open raw only for exact verification or repair.\n", "title": "Route session-memory work", "tokens": [ "1.0.0", + "1.0.1", "abi", + "acceptance", "access", "acting", "action", + "activate", "active", + "actual", + "actually", "adapter", "adapter-specific", "adapters", @@ -14517,6 +14599,7 @@ "advertised", "after", "against", + "agent", "agents", "agents.md", "alone", @@ -14530,22 +14613,30 @@ "aoa-session-memory-global-route", "apache", "apache-2.0", + "app", + "app-server", "applicability", "applicable", "architecture", "archive", + "archived", "asks", "assure", "audit", "authored", "authored-procedure", "authority", + "authorized", + "availability", "available", "baseline", "batch", "before", "behavior", + "bind", + "binding", "bindings", + "block", "both", "boundaries", "boundary", @@ -14569,30 +14660,36 @@ "chosen", "classifier", "classify", + "cli", "codex", "codex-compact-probe", "codex-history-root", "codex-hooks-status", "command", - "commands", "compact", "compaction", "composition", "concrete", + "configuration", + "consumer", "context", "continuation", + "continuity", + "contract", "corpus", "curate", "current", "dag", "data", "decision", + "declares", "deeper", "description", "design", "design.agents.md", "design.md", "diagnostics", + "different", "discovery", "distillation", "distinct", @@ -14600,9 +14697,11 @@ "doctor", "does", "effects", + "eligibility", "entrypoint", "eval", "event", + "every", "evidence", "exact", "executable", @@ -14612,7 +14711,7 @@ "explicit", "explicitly", "export", - "failures", + "exposure", "families", "filesystem", "fingerprint", @@ -14629,6 +14728,7 @@ "global-router", "globally", "graph", + "guidance", "hand", "hand-writing", "handoff", @@ -14638,14 +14738,17 @@ "historical", "history", "home", + "home-port", "honest", "hook", "hooks", "host", "identity", "if", + "imply", "import", "incidents", + "including", "independent", "independent-callable", "index", @@ -14665,13 +14768,13 @@ "invoking", "its", "itself", - "jsonl", "judgment", "keep", "known", "layers", "level", "license", + "lifecycle", "line", "live", "live-scenario-audit", @@ -14689,6 +14792,7 @@ "matches", "material", "matters", + "mcp", "md", "memory", "memory.py", @@ -14696,6 +14800,7 @@ "metadata", "missing", "mode", + "model", "more", "name", "names", @@ -14707,6 +14812,7 @@ "narrower", "narrowest", "navigation", + "needs", "negative", "no", "no-fit", @@ -14720,6 +14826,7 @@ "operation", "operations", "optional", + "others", "output", "owner", "owner-skill-procedure", @@ -14737,6 +14844,7 @@ "pipeline.md", "planner", "plans", + "port", "portability", "portable", "portable-source-root", @@ -14746,11 +14854,13 @@ "precompact", "preparing", "preserve", + "preserved", "prior", "prior-session", "probe", "procedure", "project", + "projection", "prompt", "prompt-visible", "proof", @@ -14763,8 +14873,10 @@ "read", "read-only", "read-owner-source", + "readable", "readiness", "readiness.md", + "reasoning", "receipt", "recipes", "references", @@ -14777,8 +14889,10 @@ "replace", "request", "required", + "requirements", "requiring", "resolve", + "response", "result", "resume", "retain", @@ -14787,6 +14901,7 @@ "retrieve", "review", "review-phase-name", + "role", "root", "roots", "route", @@ -14794,8 +14909,10 @@ "router", "routers", "routing", + "rule", "run", "runtime", + "same", "scenario", "scope", "scripts", @@ -14810,6 +14927,7 @@ "selection", "semantic", "separate", + "server", "session", "session-memory", "session-memory-request", @@ -14820,6 +14938,7 @@ "sessions", "sessionstart", "set", + "settings", "similarity", "skill", "skill.aoa-session-memory-global-route", @@ -14848,8 +14967,10 @@ "taxonomy", "than", "then", + "they", "three", "through", + "tool", "top", "top-level", "topical", @@ -22915,7 +23036,7 @@ }, { "contract_tokens": [ - "1.0.0", + "1.0.1", "abi", "about", "active", @@ -23154,6 +23275,7 @@ "accepted", "access", "access-plane", + "across", "action", "active", "actually", @@ -23472,6 +23594,7 @@ "honestly", "hook", "hook-receipts", + "hooks", "host", "how", "hydrate", @@ -23686,6 +23809,7 @@ "quality", "quality.skill", "quantitative", + "queries", "query", "question", "questions", @@ -23698,6 +23822,7 @@ "read", "read-only", "reading", + "reads", "ready", "reasoning", "rebuild", @@ -24042,10 +24167,10 @@ "сырой-ref", "сырым" ], - "search_text": "Entity-anchored historical question.\nquestion\nprior-session-evidence-question\nOptional typed search packet from the search capability.\nsearch_evidence\nsearch-evidence-packet\nBounded evidence packet with freshness and source refs.\npacket\nevidence-packet\nThe user asks what a component does but does not ask how it behaved previously.\nCurrent repository or runtime state is the stronger source and no historical behavior question remains.\nThe answer depends on what happened in prior .aoa sessions and must carry resolvable raw or segment refs.\navailable\nskill\nroute-evidence\nskills/aoa-session-memory-evidence-route/SKILL.md\nindependent-callable\nretain\nIt has a distinct historical-evidence trigger, evidence-packet ABI, and proof-ceiling failure modes.\nexecutable\nUse when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nread-only\nTreating route counts as truth.\nInferring invocation from selection or a skill read.\nBroad raw scanning before typed routes.\nHiding stale or truncated providers.\nbounded-judgment\nnot-applicable\nStop when the bounded packet answers the question or exposes the exact next evidence expansion.\nread-only session-memory MCP or portable CLI\narchived evidence refs\nEvery important claim has a raw\nsegment\nsession\nreceipt\nor owner ref.\nFreshness and truncation are explicit.\nSkill evidence does not overstate invocation or effect.\nskill.aoa-session-memory-evidence-route\nprior-session\nevidence\nraw-ref\nsegment-ref\nusage-chain\nmcp\nskill-attribution\nдоказательство\nистория\nиспользование\nиспользовался\nсырой-ref\nсырым\nссылкам\nраньше\nпроизошло\nпоследствие\nskill\nmanual-baseline\nchallenger\nexperimental\n1.0.0\nadvertised\nauthored\naoa-session-memory\nskills/aoa-session-memory-evidence-route/SKILL.md\nsession-memory.use.query\nskills/aoa-session-memory-evidence-route/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-memory-evidence-route/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nReturn bounded, fresh, source-linked session evidence while keeping mention, selection, behavior, verification, and consequence distinct.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nPrior-session evidence route\nauthored-procedure\n---\nname: aoa-session-memory-evidence-route\ndescription: Use when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: evidence-router\n---\n\n# aoa-session-memory-evidence-route\n\n## Trigger Boundary\n\nUse this skill for a historical behavior question whose answer must come from\nprior session evidence. Do not use it to replace current repository source,\nlive runtime state, decision authority, eval verdicts, or durable promotion.\n\nThe route finds and qualifies evidence. It never upgrades mention, prompt\nvisibility, selection, loading, or a `SKILL.md` read into invocation or\neffectiveness.\n\n## Procedure\n\n1. Resolve the logical `` and the entity kind. Start with the\n smallest typed route:\n\n - source identity or “does it exist”: `entity-registry --lookup`;\n - behavior and nearby consequence: `usage-chain --kind `;\n - goal lifecycle: `goal-lifecycles`;\n - task-to-answer chain: `task-answer-chain`;\n - exact text, command, path, error, or session id: `literal-query-plan`;\n - relation between two entities: `graph-bridge`;\n - current route quality: `live-scenario-corpus check`.\n\n2. Prefer the read-only session-memory MCP equivalent when it is actually\n available. If transport or registry freshness blocks it, name the CLI\n fallback and run the same typed route from ``. A missing MCP tool\n is a transport/runtime issue, not permission for unbounded raw search.\n\n3. Read applicability before score. Inspect freshness, ambiguity, truncation,\n omitted counts, cost profile, and the exact next expansion before opening\n another layer.\n\n4. For a skill anchor, read `skill_evidence` explicitly:\n\n - `prompt_visible`, `selected`, `skill_read`, `edited`, `mentioned`,\n `cooccurrence`, and `deflected` are candidate states only;\n - `procedure_observed`, `verified`, and `completed` require an owner receipt\n or reviewed task episode;\n - `task_episode_refs` are join keys, not proof;\n - `invocation_claim_allowed=false` is a hard claim boundary;\n - foreign-correlation results remain auditable context, never consequence.\n\n5. For a comparative skill audit about successful, failed, or\n procedure-deviating applications:\n\n - start from `usage-chain --kind skill --full` and keep its\n `task_episode_refs` only as bounded join candidates;\n - for each selected candidate, run `task-answer-chain --session \n --task-episode-id --allow-missing-reasoning\n --allow-missing-answer` before attributing a result to the skill; the\n explicit flags preserve failed, interrupted, and receipt-bearing episodes\n that have no ordinary final answer;\n - route exact reviewed receipt, error, status, or failure-class anchors\n through `literal-query-plan` with the same session and episode scope, then\n open the proposed bounded evidence window;\n - call an application successful only when reviewed result or verification\n evidence belongs to the same procedure and correlation chain;\n - treat missing positive proof as `unresolved`, not as a failed application,\n and never turn a bounded set of failures into an archive-wide absence\n claim.\n\n6. Expand only the missing evidence class. Use raw or segment refs to verify an\n important claim, exact error, or bounded interval. Keep private bodies,\n hidden reasoning, and unrelated context outside the packet.\n\n7. Return the evidence packet to the source owner, decision owner,\n `aoa-evals`, or another named authority. Do not write that owner from\n session evidence alone.\n\nFor a specialized route, open exactly one shallow reference:\n\n- [consumer-profiles.md](references/consumer-profiles.md) for typed query\n selection and packet interpretation;\n- [maintenance-and-graph.md](references/maintenance-and-graph.md) for search\n pressure, graph pressure, rollups, and shrink/prune stop-lines;\n- [mcp-fallback.md](references/mcp-fallback.md) for exact MCP tools,\n preflight, and equivalent CLI commands.\n\n## Authority Boundary\n\nSession-memory packets are generated navigation and evidence surfaces. They\nare weaker than current owner source, repository decisions, canonical skills,\ncentral eval verdicts, and reviewed promotion surfaces. Route counts and\nsemantic proximity do not establish correctness, causality, invocation, or\nbenefit.\n\n## Verification\n\n- Name the first route and why it matched the question.\n- Name MCP or explicit CLI fallback.\n- Report freshness, ambiguity, truncation, and privacy posture.\n- Cite at least one resolvable raw, segment, session, receipt, or owner ref for\n each important historical claim.\n- For skill behavior, name exact package version/fingerprint when a reviewed\n receipt provides it and list alternative explanations that remain.\n- For candidate-semantics regression, run the reviewed\n `skill_candidate_semantics_contract` corpus case and label its synthetic\n evidence origin.\n\n## Stop Line\n\nStop when the bounded packet answers the question with resolvable refs, or\nwhen it exposes one exact next expansion and the claim remains honestly\nunresolved. Never compensate for a stale provider, closed transport,\nunavailable receipt, or missing authority by widening into an unbounded\ntranscript scan.\n\n---\nname: aoa-session-memory-evidence-route\ndescription: Use when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: evidence-router\n---\n\n# aoa-session-memory-evidence-route\n\n## Trigger Boundary\n\nUse this skill for a historical behavior question whose answer must come from\nprior session evidence. Do not use it to replace current repository source,\nlive runtime state, decision authority, eval verdicts, or durable promotion.\n\nThe route finds and qualifies evidence. It never upgrades mention, prompt\nvisibility, selection, loading, or a `SKILL.md` read into invocation or\neffectiveness.\n\n## Procedure\n\n1. Resolve the logical `` and the entity kind. Start with the\n smallest typed route:\n\n - source identity or “does it exist”: `entity-registry --lookup`;\n - behavior and nearby consequence: `usage-chain --kind `;\n - goal lifecycle: `goal-lifecycles`;\n - task-to-answer chain: `task-answer-chain`;\n - exact text, command, path, error, or session id: `literal-query-plan`;\n - relation between two entities: `graph-bridge`;\n - current route quality: `live-scenario-corpus check`.\n\n2. Prefer the read-only session-memory MCP equivalent when it is actually\n available. If transport or registry freshness blocks it, name the CLI\n fallback and run the same typed route from ``. A missing MCP tool\n is a transport/runtime issue, not permission for unbounded raw search.\n\n3. Read applicability before score. Inspect freshness, ambiguity, truncation,\n omitted counts, cost profile, and the exact next expansion before opening\n another layer.\n\n4. For a skill anchor, read `skill_evidence` explicitly:\n\n - `prompt_visible`, `selected`, `skill_read`, `edited`, `mentioned`,\n `cooccurrence`, and `deflected` are candidate states only;\n - `procedure_observed`, `verified`, and `completed` require an owner receipt\n or reviewed task episode;\n - `task_episode_refs` are join keys, not proof;\n - `invocation_claim_allowed=false` is a hard claim boundary;\n - foreign-correlation results remain auditable context, never consequence.\n\n5. For a comparative skill audit about successful, failed, or\n procedure-deviating applications:\n\n - start from `usage-chain --kind skill --full` and keep its\n `task_episode_refs` only as bounded join candidates;\n - for each selected candidate, run `task-answer-chain --session \n --task-episode-id --allow-missing-reasoning\n --allow-missing-answer` before attributing a result to the skill; the\n explicit flags preserve failed, interrupted, and receipt-bearing episodes\n that have no ordinary final answer;\n - route exact reviewed receipt, error, status, or failure-class anchors\n through `literal-query-plan` with the same session and episode scope, then\n open the proposed bounded evidence window;\n - call an application successful only when reviewed result or verification\n evidence belongs to the same procedure and correlation chain;\n - treat missing positive proof as `unresolved`, not as a failed application,\n and never turn a bounded set of failures into an archive-wide absence\n claim.\n\n6. Expand only the missing evidence class. Use raw or segment refs to verify an\n important claim, exact error, or bounded interval. Keep private bodies,\n hidden reasoning, and unrelated context outside the packet.\n\n7. Return the evidence packet to the source owner, decision owner,\n `aoa-evals`, or another named authority. Do not write that owner from\n session evidence alone.\n\nFor a specialized route, open exactly one shallow reference:\n\n- [consumer-profiles.md](references/consumer-profiles.md) for typed query\n selection and packet interpretation;\n- [maintenance-and-graph.md](references/maintenance-and-graph.md) for search\n pressure, graph pressure, rollups, and shrink/prune stop-lines;\n- [mcp-fallback.md](references/mcp-fallback.md) for exact MCP tools,\n preflight, and equivalent CLI commands.\n\n## Authority Boundary\n\nSession-memory packets are generated navigation and evidence surfaces. They\nare weaker than current owner source, repository decisions, canonical skills,\ncentral eval verdicts, and reviewed promotion surfaces. Route counts and\nsemantic proximity do not establish correctness, causality, invocation, or\nbenefit.\n\n## Verification\n\n- Name the first route and why it matched the question.\n- Name MCP or explicit CLI fallback.\n- Report freshness, ambiguity, truncation, and privacy posture.\n- Cite at least one resolvable raw, segment, session, receipt, or owner ref for\n each important historical claim.\n- For skill behavior, name exact package version/fingerprint when a reviewed\n receipt provides it and list alternative explanations that remain.\n- For candidate-semantics regression, run the reviewed\n `skill_candidate_semantics_contract` corpus case and label its synthetic\n evidence origin.\n\n## Stop Line\n\nStop when the bounded packet answers the question with resolvable refs, or\nwhen it exposes one exact next expansion and the claim remains honestly\nunresolved. Never compensate for a stale provider, closed transport,\nunavailable receipt, or missing authority by widening into an unbounded\ntranscript scan.\n\n# Evidence consumer profiles\n\nUse this card only after the main route has classified the question.\n\n| Need | First route | Escalation |\n|---|---|---|\n| registered source identity | `entity-registry --lookup --kind ` | usage route only for behavior |\n| entity use and nearby consequence | `usage-chain --kind ` | `entity-dossier`, then usage audit/neighborhood |\n| hook health | `hook-receipts` | dossier or usage audit |\n| goal lifecycle | `goal-lifecycles` | returned task/answer/closeout expansion |\n| task to answer | `task-answer-chain` | one returned reasoning, answer, or closeout lane |\n| agent answer or closeout | `agent-responses` or `agent-closeouts` | bounded answer neighborhood |\n| exact literal, path, command, error, or id | `literal-query-plan` | its structured route, scoped FTS, then bounded raw |\n| two-entity relation | `graph-bridge` | bounded graph neighborhood or shortest path |\n| one dense entity neighborhood | `graph-cooccurrence` | bounded graph neighborhood |\n| route-quality regression | `live-scenario-corpus check` | one case-specific audit |\n\n## Entity identity\n\nFor an `entity-registry` packet, read `identity_status`,\n`identity_candidate_ids`, `collision_preserved`,\n`agent_route_packet.identity_claim_admitted`, and each entry's\n`canonicalization` before attributing behavior to an implementation. The\nstable typed `kind:key` is a route aggregate, not proof that multiple source\nor installed copies are one identity.\n\nSchema-v2 candidates with the same content digest may share one candidate\nidentity while retaining every owner and source ref. Distinct active\nfingerprints remain competing candidates. A legacy or incompatible registry\ngeneration is navigation-only and must route to\n`entity-registry-search-sync`; the consumer must not promote it into a\nresolved identity.\n\n## Skill evidence\n\n`state_counts` is one canonical state per archived event.\n`association_state_counts` may retain weaker projections of the same event;\ndo not add the two as independent evidence.\n\nUse these claim classes:\n\n| State | Allowed claim |\n|---|---|\n| `prompt_visible` | the exact skill entry was visible in bounded initial context |\n| `selected` | the agent or structured route selected the named skill |\n| `skill_read` | the named `SKILL.md` body was opened |\n| `procedure_observed` | a reviewed receipt links named procedure checkpoints to the task episode |\n| `verified` | a named verifier accepted declared postconditions |\n| `completed` | procedure, verification, and terminal consequence are jointly receipted |\n| `deflected` | selection or invocation was explicitly declined |\n| `edited`, `mentioned`, `cooccurrence` | artifact or contextual association only |\n\nA structured `loaded` action means the runtime embedded the skill payload. It\ndoes not prove that the model followed it. Bare task ids are session-local\njoin keys. Open the reviewed episode or receipt before an invocation claim.\n\nValidate, preview, and explicitly admit an owner-reviewed receipt with:\n\n```bash\npython3 scripts/aoa_session_memory.py skill-usage-receipt validate RECEIPT.json\npython3 scripts/aoa_session_memory.py skill-usage-receipt record RECEIPT.json\n```\n\nThe `record` command shown here is a non-mutating plan. This read-only evidence\nskill must stop there. The owner CLI exposes a separate explicit apply action\nfor an authorized reviewer; it writes one immutable receipt below the logical\nsession-memory diagnostics root. Reusing the same receipt id with different\ncontent fails closed. `list --skill ` returns only admission summaries\nand claim ceilings, not a benefit verdict.\n\nAn admitted receipt must bind source, installed, prompt-visible, selected, and\nexecuted versions; package and installed fingerprints; one task episode;\nobserved procedure sections; tools; checkpoints; a verifier; consequences;\nreview; and alternative explanations. `identity_status=drift` can prove\ninvocation of the explicitly matched historical selected/executed package\nwhen those identities agree, but it sets `source_current=false` and\n`promotion_identity_eligible=false`. It cannot prove that the current source\nwas invoked. Even a verified current receipt exposes only an\neffect-attribution candidate until `aoa-evals` resolves a controlled\ncomparison.\n\nIf `quality.skill_text_fallback_deferred=true`, the bounded dispatch passes\nfound no candidate and deliberately avoided broad FTS. This is not proof of\nabsence. Follow the returned literal/raw expansion only when exact recall\nmatters.\n\nForeign correlated results must stay under rejection/context edges with both\nsource and rejected correlation ids.\n\n## Literal and operational routes\n\nRead `literal_route_strategy`, `cost_profile`, `fallback_plan`, and\n`next_expansion_command` before opening raw. A concrete registered entity wins\nover broad class vocabulary. For exact session ids, prefer the rehydrate or\nsession route.\n\nFor coherent work, temporal intervals, quantitative comparisons, or causal\nquestions, follow the episode route selected by `literal-query-plan`. Read its\n`answer_admission`, typed relation gates, generation identities, scoped and\nglobal freshness, and evidence refs before interpreting candidates. A\n`why`/`почему` query requires one correlation-matched, chronologically ordered\naction/result chain; lexical strength, adjacency, cooccurrence, and semantic\nsimilarity cannot admit causality.\n\nExplicitly quoted temporal endpoints remain exact anchors. They are\nnavigation until one bounded, non-truncated, source-aware read proves the\ninterior. If the episode generation is missing or incompatible, use the\nread-only status and exact-raw routes from\n`episode_projection_generation_recovery`. Its deep maintenance command is an\nexplicit resource-gated mutation, and raw endpoint hits neither upgrade the\nprojection nor answer the interval.\n\n`search-operational-route-rollup-query` and\n`search-operational-direct-event-rollup-query` are compact navigation read\nmodels. They do not rebuild maintenance, use FTS, hydrate raw bodies, or prove\nbehavior. Follow typed-lane or dedicated-route advice before fuzzy results.\n\n## Packet reading\n\nPrefer packets that preserve:\n\n- normalized entity and route candidates;\n- candidate, result, consequence, and rejection counts;\n- freshness, ambiguity, truncation, and omitted counts;\n- query cost and fallback position;\n- raw, raw-block, segment, segment-index, session, and owner refs;\n- exact next expansion.\n\nOpen raw only to verify a material claim, exact error, or bounded temporal\ninterval. An ordered endpoint pair is navigation until interval contents are\nread through the source-aware bounded route.\n\n# Maintenance and graph evidence routes\n\nThis card contains operator routes that are intentionally absent from the\ncompact evidence router.\n\n## Search pressure\n\nUse `search-pressure-decision-packet` or its MCP equivalent before a heavier\nmaintenance plan. If unavailable, inspect the compact `maintenance-status`\nsearch-pressure section. Run a fresh projection plan only when counts or tail\nstate are missing or stale.\n\nTreat shrink gates as read-only evidence. `apply_ready=false` is normal until\nroute-rollup refs, exact-recall fallback, live scenarios, privacy, and\nbefore/after storage comparison are present. Run the explicit shrink-apply\nroute only after operator authorization. `applied_with_storage_warning` is not\na storage win.\n\nRoute-backed context-tail omission must leave compact rollup refs. Keep\nunrouted tails and monolith fallback until their replacement is proven.\nDirty-only maintenance may inherit the recorded omission policy; inspect its\nresolution instead of guessing rollback or slim mode.\n\n## Resource-gated completion\n\nRead `completion_semantics` before reporting an\n`auto-maintenance-resource` run. Process completion, bounded semantic\nprogress, global completion, and global freshness are independent claims.\n`completed_with_deferred_handoff` means that the selected bounded profile\nfinished its control flow and handed remaining work to a stronger profile; it\ndoes not make the projections globally current.\n\nFor timer or retry origins, require `automatic_retry.target_queue_key` or\n`handoff_queue_key` to name the stronger queued profile, and require retry\nhistory disposition `scope_completed_with_deferred_handoff`. For a manual\norigin, return the exact handoff command without claiming that background work\nwas scheduled. Neither exit code nor a successful systemd result proves\nsemantic freshness.\n\n## Generation and partial publication\n\nFor generation or partial-publish suspicion, read `projection-status` before\nwidening retrieval. A shard fan-out packet with\n`search_catalog_generation_incompatible_fallback_monolith` is an explicit\nfallback, not an empty semantic result; preserve its catalog generation and\nrefresh command.\n\nAtlas routes are usable only when the root, every referenced axis, and\n`maps/index-state.json` share the expected generation and publish epoch.\n`atlas_axis_publish_epoch_mismatch` or `atlas_publish_epoch_incomplete`\nrequires the clean Atlas rebuild route, never a scoped incremental repair. A\nclean rebuild reporting `deferred_budget_exhausted_no_publish` preserved the\nlast-good Atlas and published no new route epoch. Dense `store_failed`\nlikewise preserves prior committed session vectors; use sparse or raw fallback\nand repair that session instead of admitting the attempted dense generation.\n\n## Graph pressure\n\nFor an exact structured correlation that is absent from the graph store, pass\n`--session ` to `graph-neighborhood` or `graph-timeline` when the\nsession is known. The session is only a bounded retrieval-seed scope. Even if\nsearch is unavailable, complete digest-verified archived raw may seed the\nroute, but only exact structured correlation fields admit source events;\nmentions and foreign results remain rejected context. Preserve `--session` in\nthe packet's next command.\n\nUse `graph-high-fanout-policy` before proposing compaction or pruning. It is a\npolicy packet, not delete permission. For one dense anchor, use\n`graph-entity-usage-replacement-proof`; for several, use the reviewed\nhigh-fanout replacement corpus case.\n\nKeep `prune_gate.apply_ready=false` until replacement routes preserve source\nrefs, freshness, fallback, route quality, privacy, and before/after\ncardinality. A generated maintenance queue is scheduling state only. Raw,\nsegment, and owner sources remain authority.\n\n`waiting_for_quiet_window` means retry status now and run catch-up only after\nthe returned time gate. A bounded fallback graph drip is progress, not\ncompletion of the outer maintenance profile.\n\n## Live scenarios\n\n`live-scenario-corpus list` is source inventory, not live proof. Use\n`live-scenario-corpus check` for the reviewed regression gate and\n`live-scenario-audit` for one-off diagnostics. Preserve the packet's\n`truth_status`, evidence origin, profile, and exact check command.\n\nNo maintenance result may authorize another mutation merely because the\nprevious gate or validator is green.\n\n# MCP and CLI evidence routes\n\nPrefer the exact read-only MCP tool when the current registry exposes it:\n\n| Need | Tool |\n|---|---|\n| transport preflight | `aoa_session_transport_preflight` |\n| access-plane health | `aoa_session_access_plane_preflight` |\n| literal plan | `aoa_session_literal_query_plan` |\n| entity dossier | `aoa_session_entity_dossier` |\n| usage chain | `aoa_session_entity_usage_chain` |\n| entity inventory | `aoa_session_entity_inventory` |\n| registry lookup | `aoa_session_entity_registry` |\n| usage audit | `aoa_session_entity_usage_audit` |\n| usage neighborhood | `aoa_session_entity_usage_neighborhood` |\n| hook receipts | `aoa_session_hook_receipts` |\n| graph neighborhood | `aoa_session_graph_neighborhood` |\n| graph bridge | `aoa_session_graph_bridge` |\n| graph cooccurrence | `aoa_session_graph_cooccurrence` |\n| projection status | `aoa_session_projection_status` |\n| search pressure | `aoa_session_search_pressure_decision_packet` |\n| route rollup | `aoa_session_route_rollup_query` |\n| direct event rollup | `aoa_session_direct_event_rollup_query` |\n| live scenario | `aoa_session_live_scenario_audit` |\n| reviewed corpus check | `aoa_session_live_scenario_corpus_check` |\n\nIf an exact tool returns `Transport closed`, run transport preflight when it is\nstill callable. A configured green stdio plane with no fresh child in the\ncurrent session requires runtime reload; name the CLI fallback meanwhile.\n\nEquivalent portable commands use the resolved ``:\n\n```bash\ncd \npython3 scripts/aoa_session_memory.py usage-chain --kind \npython3 scripts/aoa_session_memory.py entity-dossier --kind \npython3 scripts/aoa_session_memory.py entity-usage-audit --kind \npython3 scripts/aoa_session_memory.py entity-usage-neighborhood --kind \npython3 scripts/aoa_session_memory.py entity-registry --lookup --kind \npython3 scripts/aoa_session_memory.py literal-query-plan \"\" --kind auto\npython3 scripts/aoa_session_memory.py projection-status\npython3 scripts/aoa_session_memory.py graph-neighborhood --kind --session --limit 12 --edge-limit 48\npython3 scripts/aoa_session_memory.py graph-bridge --source-kind --target-kind \npython3 scripts/aoa_session_memory.py live-scenario-corpus list\npython3 scripts/aoa_session_memory.py live-scenario-corpus check --case-limit 1\n```\n\nFor a fresh transport diagnosis, resolve an `abyss-stack` checkout and use the\nservice-owned preflight/validator there. Do not embed one host checkout path in\nthis portable skill.\n", + "search_text": "Entity-anchored historical question.\nquestion\nprior-session-evidence-question\nOptional typed search packet from the search capability.\nsearch_evidence\nsearch-evidence-packet\nBounded evidence packet with freshness and source refs.\npacket\nevidence-packet\nThe user asks what a component does but does not ask how it behaved previously.\nCurrent repository or runtime state is the stronger source and no historical behavior question remains.\nThe answer depends on what happened in prior .aoa sessions and must carry resolvable raw or segment refs.\navailable\nskill\nroute-evidence\nskills/aoa-session-memory-evidence-route/SKILL.md\nindependent-callable\nretain\nIt has a distinct historical-evidence trigger, evidence-packet ABI, and proof-ceiling failure modes.\nexecutable\nUse when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nread-only\nTreating route counts as truth.\nInferring invocation from selection or a skill read.\nBroad raw scanning before typed routes.\nHiding stale or truncated providers.\nbounded-judgment\nnot-applicable\nStop when the bounded packet answers the question or exposes the exact next evidence expansion.\nread-only session-memory MCP or portable CLI\narchived evidence refs\nEvery important claim has a raw\nsegment\nsession\nreceipt\nor owner ref.\nFreshness and truncation are explicit.\nSkill evidence does not overstate invocation or effect.\nskill.aoa-session-memory-evidence-route\nprior-session\nevidence\nraw-ref\nsegment-ref\nusage-chain\nmcp\nskill-attribution\nдоказательство\nистория\nиспользование\nиспользовался\nсырой-ref\nсырым\nссылкам\nраньше\nпроизошло\nпоследствие\nskill\nmanual-baseline\nchallenger\nexperimental\n1.0.1\nadvertised\nauthored\naoa-session-memory\nskills/aoa-session-memory-evidence-route/SKILL.md\nsession-memory.use.query\nskills/aoa-session-memory-evidence-route/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-memory-evidence-route/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nReturn bounded, fresh, source-linked session evidence while keeping mention, selection, behavior, verification, and consequence distinct.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nPrior-session evidence route\nauthored-procedure\n---\nname: aoa-session-memory-evidence-route\ndescription: Use when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: evidence-router\n---\n\n# aoa-session-memory-evidence-route\n\n## Trigger Boundary\n\nUse this skill for a historical behavior question whose answer must come from\nprior session evidence. Do not use it to replace current repository source,\nlive runtime state, decision authority, eval verdicts, or durable promotion.\n\nThe route finds and qualifies evidence. It never upgrades mention, prompt\nvisibility, selection, loading, or a `SKILL.md` read into invocation or\neffectiveness.\n\nBind bounded queries through the available MCP or owner CLI. Archive reads\npreserve the same evidence contract across models and do not verify live hooks.\n\n## Procedure\n\n1. Resolve the logical `` and the entity kind. Start with the\n smallest typed route:\n\n - source identity or “does it exist”: `entity-registry --lookup`;\n - behavior and nearby consequence: `usage-chain --kind `;\n - goal lifecycle: `goal-lifecycles`;\n - task-to-answer chain: `task-answer-chain`;\n - exact text, command, path, error, or session id: `literal-query-plan`;\n - relation between two entities: `graph-bridge`;\n - current route quality: `live-scenario-corpus check`.\n\n2. Prefer the read-only session-memory MCP equivalent when it is actually\n available. If transport or registry freshness blocks it, name the CLI\n fallback and run the same typed route from ``. A missing MCP tool\n is a transport/runtime issue, not permission for unbounded raw search.\n\n3. Read applicability before score. Inspect freshness, ambiguity, truncation,\n omitted counts, cost profile, and the exact next expansion before opening\n another layer.\n\n4. For a skill anchor, read `skill_evidence` explicitly:\n\n - `prompt_visible`, `selected`, `skill_read`, `edited`, `mentioned`,\n `cooccurrence`, and `deflected` are candidate states only;\n - `procedure_observed`, `verified`, and `completed` require an owner receipt\n or reviewed task episode;\n - `task_episode_refs` are join keys, not proof;\n - `invocation_claim_allowed=false` is a hard claim boundary;\n - foreign-correlation results remain auditable context, never consequence.\n\n5. For a comparative skill audit about successful, failed, or\n procedure-deviating applications:\n\n - start from `usage-chain --kind skill --full` and keep its\n `task_episode_refs` only as bounded join candidates;\n - for each selected candidate, run `task-answer-chain --session \n --task-episode-id --allow-missing-reasoning\n --allow-missing-answer` before attributing a result to the skill; the\n explicit flags preserve failed, interrupted, and receipt-bearing episodes\n that have no ordinary final answer;\n - route exact reviewed receipt, error, status, or failure-class anchors\n through `literal-query-plan` with the same session and episode scope, then\n open the proposed bounded evidence window;\n - call an application successful only when reviewed result or verification\n evidence belongs to the same procedure and correlation chain;\n - treat missing positive proof as `unresolved`, not as a failed application,\n and never turn a bounded set of failures into an archive-wide absence\n claim.\n\n6. Expand only the missing evidence class. Use raw or segment refs to verify an\n important claim, exact error, or bounded interval. Keep private bodies,\n hidden reasoning, and unrelated context outside the packet.\n\n7. Return the evidence packet to the source owner, decision owner,\n `aoa-evals`, or another named authority. Do not write that owner from\n session evidence alone.\n\nFor a specialized route, open exactly one shallow reference:\n\n- [consumer-profiles.md](references/consumer-profiles.md) for typed query\n selection and packet interpretation;\n- [maintenance-and-graph.md](references/maintenance-and-graph.md) for search\n pressure, graph pressure, rollups, and shrink/prune stop-lines;\n- [mcp-fallback.md](references/mcp-fallback.md) for exact MCP tools,\n preflight, and equivalent CLI commands.\n\n## Authority Boundary\n\nSession-memory packets are generated navigation and evidence surfaces. They\nare weaker than current owner source, repository decisions, canonical skills,\ncentral eval verdicts, and reviewed promotion surfaces. Route counts and\nsemantic proximity do not establish correctness, causality, invocation, or\nbenefit.\n\n## Verification\n\n- Name the first route and why it matched the question.\n- Name MCP or explicit CLI fallback.\n- Report freshness, ambiguity, truncation, and privacy posture.\n- Cite at least one resolvable raw, segment, session, receipt, or owner ref for\n each important historical claim.\n- For skill behavior, name exact package version/fingerprint when a reviewed\n receipt provides it and list alternative explanations that remain.\n- For candidate-semantics regression, run the reviewed\n `skill_candidate_semantics_contract` corpus case and label its synthetic\n evidence origin.\n\n## Stop Line\n\nStop when the bounded packet answers the question with resolvable refs, or\nwhen it exposes one exact next expansion and the claim remains honestly\nunresolved. Never compensate for a stale provider, closed transport,\nunavailable receipt, or missing authority by widening into an unbounded\ntranscript scan.\n\n---\nname: aoa-session-memory-evidence-route\ndescription: Use when an agent needs evidence from prior `.aoa` sessions about how a skill, MCP, hook, tool, API, goal, eval, test, validator, script, decision, error, receipt, or other recurring operational entity was used, what happened nearby, and which raw or segment refs prove it.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: evidence-router\n---\n\n# aoa-session-memory-evidence-route\n\n## Trigger Boundary\n\nUse this skill for a historical behavior question whose answer must come from\nprior session evidence. Do not use it to replace current repository source,\nlive runtime state, decision authority, eval verdicts, or durable promotion.\n\nThe route finds and qualifies evidence. It never upgrades mention, prompt\nvisibility, selection, loading, or a `SKILL.md` read into invocation or\neffectiveness.\n\nBind bounded queries through the available MCP or owner CLI. Archive reads\npreserve the same evidence contract across models and do not verify live hooks.\n\n## Procedure\n\n1. Resolve the logical `` and the entity kind. Start with the\n smallest typed route:\n\n - source identity or “does it exist”: `entity-registry --lookup`;\n - behavior and nearby consequence: `usage-chain --kind `;\n - goal lifecycle: `goal-lifecycles`;\n - task-to-answer chain: `task-answer-chain`;\n - exact text, command, path, error, or session id: `literal-query-plan`;\n - relation between two entities: `graph-bridge`;\n - current route quality: `live-scenario-corpus check`.\n\n2. Prefer the read-only session-memory MCP equivalent when it is actually\n available. If transport or registry freshness blocks it, name the CLI\n fallback and run the same typed route from ``. A missing MCP tool\n is a transport/runtime issue, not permission for unbounded raw search.\n\n3. Read applicability before score. Inspect freshness, ambiguity, truncation,\n omitted counts, cost profile, and the exact next expansion before opening\n another layer.\n\n4. For a skill anchor, read `skill_evidence` explicitly:\n\n - `prompt_visible`, `selected`, `skill_read`, `edited`, `mentioned`,\n `cooccurrence`, and `deflected` are candidate states only;\n - `procedure_observed`, `verified`, and `completed` require an owner receipt\n or reviewed task episode;\n - `task_episode_refs` are join keys, not proof;\n - `invocation_claim_allowed=false` is a hard claim boundary;\n - foreign-correlation results remain auditable context, never consequence.\n\n5. For a comparative skill audit about successful, failed, or\n procedure-deviating applications:\n\n - start from `usage-chain --kind skill --full` and keep its\n `task_episode_refs` only as bounded join candidates;\n - for each selected candidate, run `task-answer-chain --session \n --task-episode-id --allow-missing-reasoning\n --allow-missing-answer` before attributing a result to the skill; the\n explicit flags preserve failed, interrupted, and receipt-bearing episodes\n that have no ordinary final answer;\n - route exact reviewed receipt, error, status, or failure-class anchors\n through `literal-query-plan` with the same session and episode scope, then\n open the proposed bounded evidence window;\n - call an application successful only when reviewed result or verification\n evidence belongs to the same procedure and correlation chain;\n - treat missing positive proof as `unresolved`, not as a failed application,\n and never turn a bounded set of failures into an archive-wide absence\n claim.\n\n6. Expand only the missing evidence class. Use raw or segment refs to verify an\n important claim, exact error, or bounded interval. Keep private bodies,\n hidden reasoning, and unrelated context outside the packet.\n\n7. Return the evidence packet to the source owner, decision owner,\n `aoa-evals`, or another named authority. Do not write that owner from\n session evidence alone.\n\nFor a specialized route, open exactly one shallow reference:\n\n- [consumer-profiles.md](references/consumer-profiles.md) for typed query\n selection and packet interpretation;\n- [maintenance-and-graph.md](references/maintenance-and-graph.md) for search\n pressure, graph pressure, rollups, and shrink/prune stop-lines;\n- [mcp-fallback.md](references/mcp-fallback.md) for exact MCP tools,\n preflight, and equivalent CLI commands.\n\n## Authority Boundary\n\nSession-memory packets are generated navigation and evidence surfaces. They\nare weaker than current owner source, repository decisions, canonical skills,\ncentral eval verdicts, and reviewed promotion surfaces. Route counts and\nsemantic proximity do not establish correctness, causality, invocation, or\nbenefit.\n\n## Verification\n\n- Name the first route and why it matched the question.\n- Name MCP or explicit CLI fallback.\n- Report freshness, ambiguity, truncation, and privacy posture.\n- Cite at least one resolvable raw, segment, session, receipt, or owner ref for\n each important historical claim.\n- For skill behavior, name exact package version/fingerprint when a reviewed\n receipt provides it and list alternative explanations that remain.\n- For candidate-semantics regression, run the reviewed\n `skill_candidate_semantics_contract` corpus case and label its synthetic\n evidence origin.\n\n## Stop Line\n\nStop when the bounded packet answers the question with resolvable refs, or\nwhen it exposes one exact next expansion and the claim remains honestly\nunresolved. Never compensate for a stale provider, closed transport,\nunavailable receipt, or missing authority by widening into an unbounded\ntranscript scan.\n\n# Evidence consumer profiles\n\nUse this card only after the main route has classified the question.\n\n| Need | First route | Escalation |\n|---|---|---|\n| registered source identity | `entity-registry --lookup --kind ` | usage route only for behavior |\n| entity use and nearby consequence | `usage-chain --kind ` | `entity-dossier`, then usage audit/neighborhood |\n| hook health | `hook-receipts` | dossier or usage audit |\n| goal lifecycle | `goal-lifecycles` | returned task/answer/closeout expansion |\n| task to answer | `task-answer-chain` | one returned reasoning, answer, or closeout lane |\n| agent answer or closeout | `agent-responses` or `agent-closeouts` | bounded answer neighborhood |\n| exact literal, path, command, error, or id | `literal-query-plan` | its structured route, scoped FTS, then bounded raw |\n| two-entity relation | `graph-bridge` | bounded graph neighborhood or shortest path |\n| one dense entity neighborhood | `graph-cooccurrence` | bounded graph neighborhood |\n| route-quality regression | `live-scenario-corpus check` | one case-specific audit |\n\n## Entity identity\n\nFor an `entity-registry` packet, read `identity_status`,\n`identity_candidate_ids`, `collision_preserved`,\n`agent_route_packet.identity_claim_admitted`, and each entry's\n`canonicalization` before attributing behavior to an implementation. The\nstable typed `kind:key` is a route aggregate, not proof that multiple source\nor installed copies are one identity.\n\nSchema-v2 candidates with the same content digest may share one candidate\nidentity while retaining every owner and source ref. Distinct active\nfingerprints remain competing candidates. A legacy or incompatible registry\ngeneration is navigation-only and must route to\n`entity-registry-search-sync`; the consumer must not promote it into a\nresolved identity.\n\n## Skill evidence\n\n`state_counts` is one canonical state per archived event.\n`association_state_counts` may retain weaker projections of the same event;\ndo not add the two as independent evidence.\n\nUse these claim classes:\n\n| State | Allowed claim |\n|---|---|\n| `prompt_visible` | the exact skill entry was visible in bounded initial context |\n| `selected` | the agent or structured route selected the named skill |\n| `skill_read` | the named `SKILL.md` body was opened |\n| `procedure_observed` | a reviewed receipt links named procedure checkpoints to the task episode |\n| `verified` | a named verifier accepted declared postconditions |\n| `completed` | procedure, verification, and terminal consequence are jointly receipted |\n| `deflected` | selection or invocation was explicitly declined |\n| `edited`, `mentioned`, `cooccurrence` | artifact or contextual association only |\n\nA structured `loaded` action means the runtime embedded the skill payload. It\ndoes not prove that the model followed it. Bare task ids are session-local\njoin keys. Open the reviewed episode or receipt before an invocation claim.\n\nValidate, preview, and explicitly admit an owner-reviewed receipt with:\n\n```bash\npython3 scripts/aoa_session_memory.py skill-usage-receipt validate RECEIPT.json\npython3 scripts/aoa_session_memory.py skill-usage-receipt record RECEIPT.json\n```\n\nThe `record` command shown here is a non-mutating plan. This read-only evidence\nskill must stop there. The owner CLI exposes a separate explicit apply action\nfor an authorized reviewer; it writes one immutable receipt below the logical\nsession-memory diagnostics root. Reusing the same receipt id with different\ncontent fails closed. `list --skill ` returns only admission summaries\nand claim ceilings, not a benefit verdict.\n\nAn admitted receipt must bind source, installed, prompt-visible, selected, and\nexecuted versions; package and installed fingerprints; one task episode;\nobserved procedure sections; tools; checkpoints; a verifier; consequences;\nreview; and alternative explanations. `identity_status=drift` can prove\ninvocation of the explicitly matched historical selected/executed package\nwhen those identities agree, but it sets `source_current=false` and\n`promotion_identity_eligible=false`. It cannot prove that the current source\nwas invoked. Even a verified current receipt exposes only an\neffect-attribution candidate until `aoa-evals` resolves a controlled\ncomparison.\n\nIf `quality.skill_text_fallback_deferred=true`, the bounded dispatch passes\nfound no candidate and deliberately avoided broad FTS. This is not proof of\nabsence. Follow the returned literal/raw expansion only when exact recall\nmatters.\n\nForeign correlated results must stay under rejection/context edges with both\nsource and rejected correlation ids.\n\n## Literal and operational routes\n\nRead `literal_route_strategy`, `cost_profile`, `fallback_plan`, and\n`next_expansion_command` before opening raw. A concrete registered entity wins\nover broad class vocabulary. For exact session ids, prefer the rehydrate or\nsession route.\n\nFor coherent work, temporal intervals, quantitative comparisons, or causal\nquestions, follow the episode route selected by `literal-query-plan`. Read its\n`answer_admission`, typed relation gates, generation identities, scoped and\nglobal freshness, and evidence refs before interpreting candidates. A\n`why`/`почему` query requires one correlation-matched, chronologically ordered\naction/result chain; lexical strength, adjacency, cooccurrence, and semantic\nsimilarity cannot admit causality.\n\nExplicitly quoted temporal endpoints remain exact anchors. They are\nnavigation until one bounded, non-truncated, source-aware read proves the\ninterior. If the episode generation is missing or incompatible, use the\nread-only status and exact-raw routes from\n`episode_projection_generation_recovery`. Its deep maintenance command is an\nexplicit resource-gated mutation, and raw endpoint hits neither upgrade the\nprojection nor answer the interval.\n\n`search-operational-route-rollup-query` and\n`search-operational-direct-event-rollup-query` are compact navigation read\nmodels. They do not rebuild maintenance, use FTS, hydrate raw bodies, or prove\nbehavior. Follow typed-lane or dedicated-route advice before fuzzy results.\n\n## Packet reading\n\nPrefer packets that preserve:\n\n- normalized entity and route candidates;\n- candidate, result, consequence, and rejection counts;\n- freshness, ambiguity, truncation, and omitted counts;\n- query cost and fallback position;\n- raw, raw-block, segment, segment-index, session, and owner refs;\n- exact next expansion.\n\nOpen raw only to verify a material claim, exact error, or bounded temporal\ninterval. An ordered endpoint pair is navigation until interval contents are\nread through the source-aware bounded route.\n\n# Maintenance and graph evidence routes\n\nThis card contains operator routes that are intentionally absent from the\ncompact evidence router.\n\n## Search pressure\n\nUse `search-pressure-decision-packet` or its MCP equivalent before a heavier\nmaintenance plan. If unavailable, inspect the compact `maintenance-status`\nsearch-pressure section. Run a fresh projection plan only when counts or tail\nstate are missing or stale.\n\nTreat shrink gates as read-only evidence. `apply_ready=false` is normal until\nroute-rollup refs, exact-recall fallback, live scenarios, privacy, and\nbefore/after storage comparison are present. Run the explicit shrink-apply\nroute only after operator authorization. `applied_with_storage_warning` is not\na storage win.\n\nRoute-backed context-tail omission must leave compact rollup refs. Keep\nunrouted tails and monolith fallback until their replacement is proven.\nDirty-only maintenance may inherit the recorded omission policy; inspect its\nresolution instead of guessing rollback or slim mode.\n\n## Resource-gated completion\n\nRead `completion_semantics` before reporting an\n`auto-maintenance-resource` run. Process completion, bounded semantic\nprogress, global completion, and global freshness are independent claims.\n`completed_with_deferred_handoff` means that the selected bounded profile\nfinished its control flow and handed remaining work to a stronger profile; it\ndoes not make the projections globally current.\n\nFor timer or retry origins, require `automatic_retry.target_queue_key` or\n`handoff_queue_key` to name the stronger queued profile, and require retry\nhistory disposition `scope_completed_with_deferred_handoff`. For a manual\norigin, return the exact handoff command without claiming that background work\nwas scheduled. Neither exit code nor a successful systemd result proves\nsemantic freshness.\n\n## Generation and partial publication\n\nFor generation or partial-publish suspicion, read `projection-status` before\nwidening retrieval. A shard fan-out packet with\n`search_catalog_generation_incompatible_fallback_monolith` is an explicit\nfallback, not an empty semantic result; preserve its catalog generation and\nrefresh command.\n\nAtlas routes are usable only when the root, every referenced axis, and\n`maps/index-state.json` share the expected generation and publish epoch.\n`atlas_axis_publish_epoch_mismatch` or `atlas_publish_epoch_incomplete`\nrequires the clean Atlas rebuild route, never a scoped incremental repair. A\nclean rebuild reporting `deferred_budget_exhausted_no_publish` preserved the\nlast-good Atlas and published no new route epoch. Dense `store_failed`\nlikewise preserves prior committed session vectors; use sparse or raw fallback\nand repair that session instead of admitting the attempted dense generation.\n\n## Graph pressure\n\nFor an exact structured correlation that is absent from the graph store, pass\n`--session ` to `graph-neighborhood` or `graph-timeline` when the\nsession is known. The session is only a bounded retrieval-seed scope. Even if\nsearch is unavailable, complete digest-verified archived raw may seed the\nroute, but only exact structured correlation fields admit source events;\nmentions and foreign results remain rejected context. Preserve `--session` in\nthe packet's next command.\n\nUse `graph-high-fanout-policy` before proposing compaction or pruning. It is a\npolicy packet, not delete permission. For one dense anchor, use\n`graph-entity-usage-replacement-proof`; for several, use the reviewed\nhigh-fanout replacement corpus case.\n\nKeep `prune_gate.apply_ready=false` until replacement routes preserve source\nrefs, freshness, fallback, route quality, privacy, and before/after\ncardinality. A generated maintenance queue is scheduling state only. Raw,\nsegment, and owner sources remain authority.\n\n`waiting_for_quiet_window` means retry status now and run catch-up only after\nthe returned time gate. A bounded fallback graph drip is progress, not\ncompletion of the outer maintenance profile.\n\n## Live scenarios\n\n`live-scenario-corpus list` is source inventory, not live proof. Use\n`live-scenario-corpus check` for the reviewed regression gate and\n`live-scenario-audit` for one-off diagnostics. Preserve the packet's\n`truth_status`, evidence origin, profile, and exact check command.\n\nNo maintenance result may authorize another mutation merely because the\nprevious gate or validator is green.\n\n# MCP and CLI evidence routes\n\nPrefer the exact read-only MCP tool when the current registry exposes it:\n\n| Need | Tool |\n|---|---|\n| transport preflight | `aoa_session_transport_preflight` |\n| access-plane health | `aoa_session_access_plane_preflight` |\n| literal plan | `aoa_session_literal_query_plan` |\n| entity dossier | `aoa_session_entity_dossier` |\n| usage chain | `aoa_session_entity_usage_chain` |\n| entity inventory | `aoa_session_entity_inventory` |\n| registry lookup | `aoa_session_entity_registry` |\n| usage audit | `aoa_session_entity_usage_audit` |\n| usage neighborhood | `aoa_session_entity_usage_neighborhood` |\n| hook receipts | `aoa_session_hook_receipts` |\n| graph neighborhood | `aoa_session_graph_neighborhood` |\n| graph bridge | `aoa_session_graph_bridge` |\n| graph cooccurrence | `aoa_session_graph_cooccurrence` |\n| projection status | `aoa_session_projection_status` |\n| search pressure | `aoa_session_search_pressure_decision_packet` |\n| route rollup | `aoa_session_route_rollup_query` |\n| direct event rollup | `aoa_session_direct_event_rollup_query` |\n| live scenario | `aoa_session_live_scenario_audit` |\n| reviewed corpus check | `aoa_session_live_scenario_corpus_check` |\n\nIf an exact tool returns `Transport closed`, run transport preflight when it is\nstill callable. A configured green stdio plane with no fresh child in the\ncurrent session requires runtime reload; name the CLI fallback meanwhile.\n\nEquivalent portable commands use the resolved ``:\n\n```bash\ncd \npython3 scripts/aoa_session_memory.py usage-chain --kind \npython3 scripts/aoa_session_memory.py entity-dossier --kind \npython3 scripts/aoa_session_memory.py entity-usage-audit --kind \npython3 scripts/aoa_session_memory.py entity-usage-neighborhood --kind \npython3 scripts/aoa_session_memory.py entity-registry --lookup --kind \npython3 scripts/aoa_session_memory.py literal-query-plan \"\" --kind auto\npython3 scripts/aoa_session_memory.py projection-status\npython3 scripts/aoa_session_memory.py graph-neighborhood --kind --session --limit 12 --edge-limit 48\npython3 scripts/aoa_session_memory.py graph-bridge --source-kind --target-kind \npython3 scripts/aoa_session_memory.py live-scenario-corpus list\npython3 scripts/aoa_session_memory.py live-scenario-corpus check --case-limit 1\n```\n\nFor a fresh transport diagnosis, resolve an `abyss-stack` checkout and use the\nservice-owned preflight/validator there. Do not embed one host checkout path in\nthis portable skill.\n", "title": "Prior-session evidence route", "tokens": [ - "1.0.0", + "1.0.1", "12", "48", "abi", @@ -24057,6 +24182,7 @@ "accepted", "access", "access-plane", + "across", "action", "active", "actually", @@ -24407,6 +24533,7 @@ "honestly", "hook", "hook-receipts", + "hooks", "host", "how", "hydrate", @@ -24639,6 +24766,7 @@ "quality", "quality.skill", "quantitative", + "queries", "query", "question", "questions", @@ -24653,6 +24781,7 @@ "read-only", "read-owner-source", "reading", + "reads", "ready", "reasoning", "rebuild", @@ -24929,7 +25058,7 @@ }, { "contract_tokens": [ - "1.0.0", + "1.0.1", "abi", "access", "active", @@ -24937,6 +25066,7 @@ "adapter-specific", "advertised", "after", + "agent", "alone", "already", "anchor", @@ -24946,6 +25076,7 @@ "aoa-session-memory-global-route", "applicability", "applicable", + "archive", "authored", "authored-procedure", "authority", @@ -24962,7 +25093,6 @@ "capability-home-v1", "capability-selection", "challenger", - "codex", "command", "compact", "compaction", @@ -24976,7 +25106,6 @@ "exhausted", "experimental", "explicit", - "failures", "families", "filesystem", "fit", @@ -24999,6 +25128,7 @@ "known", "line", "logical", + "lookup", "manual", "manual-baseline", "match", @@ -25008,6 +25138,7 @@ "names", "narrower", "narrowest", + "needs", "negative", "no", "no-fit", @@ -25023,8 +25154,8 @@ "owner-skill-procedure", "package", "preserve", - "prior", "procedure", + "projection", "proof", "raw", "read", @@ -25037,6 +25168,7 @@ "resolve", "result", "retain", + "retrieval", "roots", "route", "router", @@ -25078,6 +25210,7 @@ "when", "widening", "without", + "work", "yaml", "архив", "маршрутизац", @@ -25085,7 +25218,7 @@ "сесс", "сжат" ], - "description": "Use in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.", + "description": "Use when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.", "id": "skill.aoa-session-memory-global-route", "kind": "skill", "negative_phrases": [ @@ -25108,18 +25241,24 @@ "session-memory" ], "package_tokens": [ + "acceptance", "access", "acting", "action", + "activate", "active", + "actual", + "actually", + "adapter", + "adapter-specific", "adapters", "adapters.codex", "admitted", "against", + "agent", "agents", "agents.md", "alone", - "any", "aoa", "aoa-root", "aoa-search", @@ -25127,24 +25266,34 @@ "aoa-session-memory-global-route", "apache", "apache-2.0", + "app", + "app-server", "applicability", "applicable", "architecture", "archive", + "archived", "asks", "assure", "audit", + "authority", + "authorized", + "availability", "available", "batch", "before", "behavior", + "bind", + "binding", "bindings", + "block", "both", "boundaries", "boundary", "branches", "bundle", "candidate", + "capabilities", "capability", "capability-router.md", "capture", @@ -25155,41 +25304,52 @@ "chosen", "classifier", "classify", + "cli", "codex", "codex-compact-probe", "codex-history-root", "codex-hooks-status", - "commands", + "command", "compact", "compaction", "composition", "concrete", + "configuration", + "consumer", "context", "continuation", + "continuity", + "contract", "corpus", "curate", "current", "dag", "decision", + "declares", "deeper", "description", "design", "design.agents.md", "design.md", "diagnostics", + "different", "discovery", "distillation", "do", "doctor", "does", "effects", + "eligibility", "eval", "event", + "every", "evidence", "exact", + "execution", "explicitly", "export", - "failures", + "exposure", + "filesystem", "fingerprint", "first", "first-wave", @@ -25200,18 +25360,24 @@ "global", "global-router", "graph", + "guidance", "hand", "hand-writing", "hash", "historical", "history", + "home", + "home-port", "hook", "hooks", "host", "identity", "if", + "imply", "import", "incidents", + "including", + "independent", "index", "index.md", "indexes", @@ -25226,12 +25392,12 @@ "invocation", "its", "itself", - "jsonl", "keep", "known", "layers", "level", "license", + "lifecycle", "line", "live", "live-scenario-audit", @@ -25246,13 +25412,14 @@ "matches", "material", "matters", + "mcp", "md", "memory", "memory.py", - "mentions", "metadata", "missing", "mode", + "model", "more", "name", "names", @@ -25261,13 +25428,16 @@ "naming-readiness", "naming-wave", "naming.md", + "needs", "negative", "not", "one", "only", "open", + "operation", "operations", "optional", + "others", "owner", "owning", "package", @@ -25282,6 +25452,7 @@ "pipeline.md", "planner", "plans", + "port", "portability", "portable", "portable-source-root", @@ -25290,11 +25461,13 @@ "posture", "precompact", "preparing", + "preserved", "prior", "prior-session", "probe", "procedure", "project", + "projection", "prompt", "prompt-visible", "proof", @@ -25305,8 +25478,11 @@ "queues", "raw", "read", + "read-only", + "readable", "readiness", "readiness.md", + "reasoning", "receipt", "recipes", "references", @@ -25318,13 +25494,16 @@ "repair", "replace", "required", + "requirements", "resolve", + "response", "resume", "retrieval", "retrieval-packet", "retrieve", "review", "review-phase-name", + "role", "root", "roots", "route", @@ -25332,8 +25511,10 @@ "router", "routers", "routing", + "rule", "run", "runtime", + "same", "scenario", "scope", "scripts", @@ -25347,16 +25528,19 @@ "selection", "semantic", "separate", + "server", "session", "session-memory", "session-name", "sessions", "sessionstart", "set", + "settings", "similarity", "skill", "smallest", "source", + "specific", "sqlite", "stable", "stale", @@ -25376,7 +25560,9 @@ "taxonomy", "than", "then", + "they", "three", + "tool", "top", "top-level", "topical", @@ -25409,33 +25595,40 @@ "workspace-root", "writing" ], - "positive_text": "skill.aoa-session-memory-global-route\nGlobal session-memory router\nUse in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.\naoa\nsession-memory\ntranscripts\ncompaction\nrehydration\nhooks\nrouter\nпамять\nсессия\nархив\nсжатие\nмаршрутизация\nThe request names session memory\n.aoa\ntranscripts\ncompaction\nrehydration\nhooks\nor session-memory validation.", + "positive_text": "skill.aoa-session-memory-global-route\nGlobal session-memory router\nUse when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.\naoa\nsession-memory\ntranscripts\ncompaction\nrehydration\nhooks\nrouter\nпамять\nсессия\nархив\nсжатие\nмаршрутизация\nThe request names session memory\n.aoa\ntranscripts\ncompaction\nrehydration\nhooks\nor session-memory validation.", "positive_tokens": [ - "any", + "adapter", + "adapter-specific", + "agent", "aoa", - "codex", + "archive", "compaction", - "failures", + "evidence", "global", "hook", "hooks", + "lookup", "memory", - "mentions", "names", - "prior", + "needs", + "operation", + "projection", "rehydration", "request", + "retrieval", "route", "router", "session", "session-memory", "skill", "skill.aoa-session-memory-global-route", + "specific", + "transcript", "transcripts", "use", - "user", "validation", "when", + "work", "архив", "маршрутизац", "памят", @@ -25458,15 +25651,19 @@ "сесс", "сжат" ], - "search_text": "One user request requiring owner routing.\nrequest\nsession-memory-request\nOne bounded capability selection or explicit handoff.\nselection\ncapability-selection\nThe request mentions memory generically without a session\ntranscript\nor .aoa anchor.\nA narrower non-session-memory owner is already known and no session evidence or session-memory operation is required.\nThe request names session memory\n.aoa\ntranscripts\ncompaction\nrehydration\nhooks\nor session-memory validation.\navailable\nskill\nroute\nskills/aoa-session-memory-global-route/SKILL.md\nindependent-callable\nretain\nIt is the compact globally visible entrypoint and has a distinct routing output ABI.\nexecutable\nUse in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.\nread-only\nSelecting from topical similarity alone.\nTreating generated routing output as procedure authority.\nWidening to raw evidence before a typed route is exhausted.\nbounded-judgment\nnot-applicable\nStop after one bounded selection\nhandoff\nor honest no-fit result.\nfilesystem read access\nsession-memory capability router\nThe selected package trigger and negative applicability match the request.\nThe selected owner and effects are stated before execution.\nskill.aoa-session-memory-global-route\naoa\nsession-memory\ntranscripts\ncompaction\nrehydration\nhooks\nrouter\nпамять\nсессия\nархив\nсжатие\nмаршрутизация\nskill\nmanual-baseline\nchallenger\nexperimental\n1.0.0\nadvertised\nauthored\naoa-session-memory\nskills/aoa-session-memory-global-route/SKILL.md\nsession-memory.use.route\nskills/aoa-session-memory-global-route/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-memory-global-route/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nSelect the narrowest owner capability and preserve source, runtime, and proof boundaries.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nGlobal session-memory router\nauthored-procedure\n---\nname: aoa-session-memory-global-route\ndescription: Use in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: global-router\n---\n\n# aoa-session-memory-global-route\n\nUse this as the top-level router for the AoA session-memory bundle.\n\n## Source Root\n\nResolve three logical roots before acting:\n\n- ``: the selected project workspace;\n- ``: the active installed session-memory root;\n- ``: the standalone owner source when source/export\n work is required.\n\nConcrete paths are runtime bindings, not portable skill identity.\n\n## Trigger Boundary\n\nUse this skill in any Codex session when the task touches:\n\n- `.aoa` session memory\n- Codex raw transcript JSONL\n- context compaction or compaction intervals\n- prior-session resume, rehydration, or session archive lookup\n- AoA hooks for `SessionStart`, `UserPromptSubmit`, `PreCompact`,\n `PostCompact`, or `Stop`\n- `raw_unavailable` incidents\n- `stress-pass`, `audit`, `doctor`, `codex-hooks-status`, or\n `codex-compact-probe`\n- event taxonomy, classifier, generated segment indexes, or `reindex-sessions`\n- portable SQLite search, `search-index`, `aoa-search`,\n `search-provider-status`, optional host provider gates, or retrieval\n freshness\n- retrieval packets, `retrieve`, `retrieval-packet`, or long-session\n continuation recipes\n- live route-quality checks, `live-scenario-audit`, or\n `live-scenario-corpus`\n- naming readiness, `SESSION_NAMES.md`, `sessions/INDEX.md`, or\n `naming-readiness`\n- mass session naming, `naming-wave`, semantic session-name review plans, or\n naming quality audit\n- phase discovery, `phase-discovery`, `review-phase-name`, or session naming\n candidate layers\n- historical Codex session import from ``\n- first-wave batch distillation or historical session review queues\n- preparing or validating the portable `aoa-session-memory` bundle\n\n## Procedure\n\n1. Read `/AGENTS.md` and classify the task before loading\n deeper context.\n2. Read only the owning surface: `DESIGN.md` for architecture or boundaries,\n `DESIGN.AGENTS.md` for query/access behavior, the relevant `PIPELINE.md`\n section for operations, `INSTALL.md` for portability, `NAMING.md` for names,\n or `READINESS.md` for proof posture.\n3. Read\n [references/capability-router.md](references/capability-router.md), verify\n its source hash against the generated graph when composition matters, and\n choose the smallest applicable bundle.\n4. Use `/scripts/aoa_session_memory.py` for commands.\n5. Keep historical raw/session material intact unless the user explicitly asks\n for a repair.\n6. If the task changes portable behavior, export to\n `` and validate both source and\n standalone surfaces.\n7. If the user-level router itself is missing or stale, run\n `install-user-skill` from the active install root instead of hand-writing a\n symlink.\n\n## Skill Routing\n\nUse the generated router card for exact package names, positive and negative\napplicability, visibility, version, and fingerprint. Its stable branches are:\n\n- `use.route` for top-level selection;\n- `use.query` for evidence, search, and rehydration;\n- `stewardship.capture`, `.project`, `.curate`, `.name`, and `.assure`;\n- `adapters.codex` for Codex hooks, transcripts, and compaction.\n\nFor more than one capability, use the full generated graph and task-local DAG\nplanner. Do not select a set from topical similarity alone.\n\n## Verification\n\n- The chosen capability's positive trigger matches and its negative trigger\n does not.\n- The selected package version/fingerprint matches the current generated\n router and installed runtime receipt when available.\n- Required inputs, permissions, effects, and verifier are known before action.\n- The prompt-visible set remains the two admitted routers unless a separate\n routing eval and owner decision changes it.\n\n## Stop Line\n\nDo not replace raw evidence with summaries. Use indexes and diagnostics first,\nthen open raw only for exact verification or repair.\n\n---\nname: aoa-session-memory-global-route\ndescription: Use in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation.\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: global-router\n---\n\n# aoa-session-memory-global-route\n\nUse this as the top-level router for the AoA session-memory bundle.\n\n## Source Root\n\nResolve three logical roots before acting:\n\n- ``: the selected project workspace;\n- ``: the active installed session-memory root;\n- ``: the standalone owner source when source/export\n work is required.\n\nConcrete paths are runtime bindings, not portable skill identity.\n\n## Trigger Boundary\n\nUse this skill in any Codex session when the task touches:\n\n- `.aoa` session memory\n- Codex raw transcript JSONL\n- context compaction or compaction intervals\n- prior-session resume, rehydration, or session archive lookup\n- AoA hooks for `SessionStart`, `UserPromptSubmit`, `PreCompact`,\n `PostCompact`, or `Stop`\n- `raw_unavailable` incidents\n- `stress-pass`, `audit`, `doctor`, `codex-hooks-status`, or\n `codex-compact-probe`\n- event taxonomy, classifier, generated segment indexes, or `reindex-sessions`\n- portable SQLite search, `search-index`, `aoa-search`,\n `search-provider-status`, optional host provider gates, or retrieval\n freshness\n- retrieval packets, `retrieve`, `retrieval-packet`, or long-session\n continuation recipes\n- live route-quality checks, `live-scenario-audit`, or\n `live-scenario-corpus`\n- naming readiness, `SESSION_NAMES.md`, `sessions/INDEX.md`, or\n `naming-readiness`\n- mass session naming, `naming-wave`, semantic session-name review plans, or\n naming quality audit\n- phase discovery, `phase-discovery`, `review-phase-name`, or session naming\n candidate layers\n- historical Codex session import from ``\n- first-wave batch distillation or historical session review queues\n- preparing or validating the portable `aoa-session-memory` bundle\n\n## Procedure\n\n1. Read `/AGENTS.md` and classify the task before loading\n deeper context.\n2. Read only the owning surface: `DESIGN.md` for architecture or boundaries,\n `DESIGN.AGENTS.md` for query/access behavior, the relevant `PIPELINE.md`\n section for operations, `INSTALL.md` for portability, `NAMING.md` for names,\n or `READINESS.md` for proof posture.\n3. Read\n [references/capability-router.md](references/capability-router.md), verify\n its source hash against the generated graph when composition matters, and\n choose the smallest applicable bundle.\n4. Use `/scripts/aoa_session_memory.py` for commands.\n5. Keep historical raw/session material intact unless the user explicitly asks\n for a repair.\n6. If the task changes portable behavior, export to\n `` and validate both source and\n standalone surfaces.\n7. If the user-level router itself is missing or stale, run\n `install-user-skill` from the active install root instead of hand-writing a\n symlink.\n\n## Skill Routing\n\nUse the generated router card for exact package names, positive and negative\napplicability, visibility, version, and fingerprint. Its stable branches are:\n\n- `use.route` for top-level selection;\n- `use.query` for evidence, search, and rehydration;\n- `stewardship.capture`, `.project`, `.curate`, `.name`, and `.assure`;\n- `adapters.codex` for Codex hooks, transcripts, and compaction.\n\nFor more than one capability, use the full generated graph and task-local DAG\nplanner. Do not select a set from topical similarity alone.\n\n## Verification\n\n- The chosen capability's positive trigger matches and its negative trigger\n does not.\n- The selected package version/fingerprint matches the current generated\n router and installed runtime receipt when available.\n- Required inputs, permissions, effects, and verifier are known before action.\n- The prompt-visible set remains the two admitted routers unless a separate\n routing eval and owner decision changes it.\n\n## Stop Line\n\nDo not replace raw evidence with summaries. Use indexes and diagnostics first,\nthen open raw only for exact verification or repair.\n", + "search_text": "One user request requiring owner routing.\nrequest\nsession-memory-request\nOne bounded capability selection or explicit handoff.\nselection\ncapability-selection\nThe request mentions memory generically without a session\ntranscript\nor .aoa anchor.\nA narrower non-session-memory owner is already known and no session evidence or session-memory operation is required.\nThe request names session memory\n.aoa\ntranscripts\ncompaction\nrehydration\nhooks\nor session-memory validation.\navailable\nskill\nroute\nskills/aoa-session-memory-global-route/SKILL.md\nindependent-callable\nretain\nIt is the compact globally visible entrypoint and has a distinct routing output ABI.\nexecutable\nUse when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.\nread-only\nSelecting from topical similarity alone.\nTreating generated routing output as procedure authority.\nWidening to raw evidence before a typed route is exhausted.\nbounded-judgment\nnot-applicable\nStop after one bounded selection\nhandoff\nor honest no-fit result.\nfilesystem read access\nsession-memory capability router\nThe selected package trigger and negative applicability match the request.\nThe selected owner and effects are stated before execution.\nskill.aoa-session-memory-global-route\naoa\nsession-memory\ntranscripts\ncompaction\nrehydration\nhooks\nrouter\nпамять\nсессия\nархив\nсжатие\nмаршрутизация\nskill\nmanual-baseline\nchallenger\nexperimental\n1.0.1\nadvertised\nauthored\naoa-session-memory\nskills/aoa-session-memory-global-route/SKILL.md\nsession-memory.use.route\nskills/aoa-session-memory-global-route/SKILL.md\nTrigger Boundary\nProcedure\nVerification\nStop Line\nResolve logical roots through the active installation before invoking any adapter-specific command.\nauthored\nskills/aoa-session-memory-global-route/SKILL.md\ncapability-home-v1\naoa-session-memory\nowner-skill-procedure\nSelect the narrowest owner capability and preserve source, runtime, and proof boundaries.\nforbidden\nread-owner-source\ntreat-as-data\nsession-memory\ncapabilities/families/session-memory.yaml\nGlobal session-memory router\nauthored-procedure\n---\nname: aoa-session-memory-global-route\ndescription: \"Use when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.\"\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: global-router\n---\n\n# aoa-session-memory-global-route\n\nUse this as the top-level router for the AoA session-memory bundle.\n\n## Source Root\n\nResolve three logical roots before acting:\n\n- ``: the selected project workspace;\n- ``: the active installed session-memory root;\n- ``: the standalone owner source when source/export\n work is required.\n\nConcrete paths are runtime bindings, not portable skill identity.\n\n## Required Capabilities\n\nSelection needs readable owner guidance and the capability router. Execution\nneeds the selected operation's actual inputs, permissions, and an available\nread-only MCP or owner CLI binding. A filesystem read, a query response, and\nan adapter lifecycle event are different capabilities; availability of one\ndoes not imply the others.\n\nUse the same evidence and scope contract for every consumer. Model and\nreasoning settings are execution configuration, not role identity, authority,\nor a different acceptance rule. The home-port's consumer exposure declares\neligibility only; it does not install or activate a runtime.\n\n## Trigger Boundary\n\nUse this skill when the task touches:\n\n- `.aoa` session memory\n- preserved session evidence or raw transcripts\n- context continuity or archived compaction intervals\n- prior-session resume, rehydration, or session archive lookup\n- capture hooks, including the Codex adapter's `SessionStart`,\n `UserPromptSubmit`, `PreCompact`, `PostCompact`, or `Stop`\n- `raw_unavailable` incidents\n- `stress-pass`, `audit`, `doctor`, `codex-hooks-status`, or\n `codex-compact-probe`\n- event taxonomy, classifier, generated segment indexes, or `reindex-sessions`\n- portable SQLite search, `search-index`, `aoa-search`,\n `search-provider-status`, optional host provider gates, or retrieval\n freshness\n- retrieval packets, `retrieve`, `retrieval-packet`, or long-session\n continuation recipes\n- live route-quality checks, `live-scenario-audit`, or\n `live-scenario-corpus`\n- naming readiness, `SESSION_NAMES.md`, `sessions/INDEX.md`, or\n `naming-readiness`\n- mass session naming, `naming-wave`, semantic session-name review plans, or\n naming quality audit\n- phase discovery, `phase-discovery`, `review-phase-name`, or session naming\n candidate layers\n- historical Codex session import from ``\n- first-wave batch distillation or historical session review queues\n- preparing or validating the portable `aoa-session-memory` bundle\n\n## Procedure\n\n1. Read `/AGENTS.md` and classify the task before loading\n deeper context.\n2. Read only the owning surface: `DESIGN.md` for architecture or boundaries,\n `DESIGN.AGENTS.md` for query/access behavior, the relevant `PIPELINE.md`\n section for operations, `INSTALL.md` for portability, `NAMING.md` for names,\n or `READINESS.md` for proof posture.\n3. Read\n [references/capability-router.md](references/capability-router.md), verify\n its source hash against the generated graph when composition matters, and\n choose the smallest applicable bundle.\n4. Bind the selected operation to an actually available read-only MCP tool or\n `/scripts/aoa_session_memory.py` CLI command. For Codex hooks,\n transcript import, app-server or live compaction, select `adapters.codex`\n and its exact requirements. Missing adapter capabilities block that\n operation; they do not block an independent authorized archive read.\n5. Keep historical raw/session material intact unless the user explicitly asks\n for a repair.\n6. If the task changes portable behavior, export to\n `` and validate both source and\n standalone surfaces.\n7. If the user-level router itself is missing or stale, run\n `install-user-skill` from the active install root instead of hand-writing a\n symlink.\n\n## Skill Routing\n\nUse the generated router card for exact package names, positive and negative\napplicability, visibility, version, and fingerprint. Its stable branches are:\n\n- `use.route` for top-level selection;\n- `use.query` for evidence, search, and rehydration;\n- `stewardship.capture`, `.project`, `.curate`, `.name`, and `.assure`;\n- `adapters.codex` for Codex hooks, transcripts, and compaction.\n\nFor more than one capability, use the full generated graph and task-local DAG\nplanner. Do not select a set from topical similarity alone.\n\n## Verification\n\n- The chosen capability's positive trigger matches and its negative trigger\n does not.\n- The selected package version/fingerprint matches the current generated\n router and installed runtime receipt when available.\n- Required inputs, permissions, effects, and verifier are known before action.\n- The prompt-visible set remains the two admitted routers unless a separate\n routing eval and owner decision changes it.\n\n## Stop Line\n\nDo not replace raw evidence with summaries. Use indexes and diagnostics first,\nthen open raw only for exact verification or repair.\n\n---\nname: aoa-session-memory-global-route\ndescription: \"Use when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation.\"\nlicense: Apache-2.0\nmetadata:\n aoa_scope: session-memory\n aoa_invocation_mode: global-router\n---\n\n# aoa-session-memory-global-route\n\nUse this as the top-level router for the AoA session-memory bundle.\n\n## Source Root\n\nResolve three logical roots before acting:\n\n- ``: the selected project workspace;\n- ``: the active installed session-memory root;\n- ``: the standalone owner source when source/export\n work is required.\n\nConcrete paths are runtime bindings, not portable skill identity.\n\n## Required Capabilities\n\nSelection needs readable owner guidance and the capability router. Execution\nneeds the selected operation's actual inputs, permissions, and an available\nread-only MCP or owner CLI binding. A filesystem read, a query response, and\nan adapter lifecycle event are different capabilities; availability of one\ndoes not imply the others.\n\nUse the same evidence and scope contract for every consumer. Model and\nreasoning settings are execution configuration, not role identity, authority,\nor a different acceptance rule. The home-port's consumer exposure declares\neligibility only; it does not install or activate a runtime.\n\n## Trigger Boundary\n\nUse this skill when the task touches:\n\n- `.aoa` session memory\n- preserved session evidence or raw transcripts\n- context continuity or archived compaction intervals\n- prior-session resume, rehydration, or session archive lookup\n- capture hooks, including the Codex adapter's `SessionStart`,\n `UserPromptSubmit`, `PreCompact`, `PostCompact`, or `Stop`\n- `raw_unavailable` incidents\n- `stress-pass`, `audit`, `doctor`, `codex-hooks-status`, or\n `codex-compact-probe`\n- event taxonomy, classifier, generated segment indexes, or `reindex-sessions`\n- portable SQLite search, `search-index`, `aoa-search`,\n `search-provider-status`, optional host provider gates, or retrieval\n freshness\n- retrieval packets, `retrieve`, `retrieval-packet`, or long-session\n continuation recipes\n- live route-quality checks, `live-scenario-audit`, or\n `live-scenario-corpus`\n- naming readiness, `SESSION_NAMES.md`, `sessions/INDEX.md`, or\n `naming-readiness`\n- mass session naming, `naming-wave`, semantic session-name review plans, or\n naming quality audit\n- phase discovery, `phase-discovery`, `review-phase-name`, or session naming\n candidate layers\n- historical Codex session import from ``\n- first-wave batch distillation or historical session review queues\n- preparing or validating the portable `aoa-session-memory` bundle\n\n## Procedure\n\n1. Read `/AGENTS.md` and classify the task before loading\n deeper context.\n2. Read only the owning surface: `DESIGN.md` for architecture or boundaries,\n `DESIGN.AGENTS.md` for query/access behavior, the relevant `PIPELINE.md`\n section for operations, `INSTALL.md` for portability, `NAMING.md` for names,\n or `READINESS.md` for proof posture.\n3. Read\n [references/capability-router.md](references/capability-router.md), verify\n its source hash against the generated graph when composition matters, and\n choose the smallest applicable bundle.\n4. Bind the selected operation to an actually available read-only MCP tool or\n `/scripts/aoa_session_memory.py` CLI command. For Codex hooks,\n transcript import, app-server or live compaction, select `adapters.codex`\n and its exact requirements. Missing adapter capabilities block that\n operation; they do not block an independent authorized archive read.\n5. Keep historical raw/session material intact unless the user explicitly asks\n for a repair.\n6. If the task changes portable behavior, export to\n `` and validate both source and\n standalone surfaces.\n7. If the user-level router itself is missing or stale, run\n `install-user-skill` from the active install root instead of hand-writing a\n symlink.\n\n## Skill Routing\n\nUse the generated router card for exact package names, positive and negative\napplicability, visibility, version, and fingerprint. Its stable branches are:\n\n- `use.route` for top-level selection;\n- `use.query` for evidence, search, and rehydration;\n- `stewardship.capture`, `.project`, `.curate`, `.name`, and `.assure`;\n- `adapters.codex` for Codex hooks, transcripts, and compaction.\n\nFor more than one capability, use the full generated graph and task-local DAG\nplanner. Do not select a set from topical similarity alone.\n\n## Verification\n\n- The chosen capability's positive trigger matches and its negative trigger\n does not.\n- The selected package version/fingerprint matches the current generated\n router and installed runtime receipt when available.\n- Required inputs, permissions, effects, and verifier are known before action.\n- The prompt-visible set remains the two admitted routers unless a separate\n routing eval and owner decision changes it.\n\n## Stop Line\n\nDo not replace raw evidence with summaries. Use indexes and diagnostics first,\nthen open raw only for exact verification or repair.\n", "title": "Global session-memory router", "tokens": [ - "1.0.0", + "1.0.1", "abi", + "acceptance", "access", "acting", "action", + "activate", "active", + "actual", + "actually", "adapter", "adapter-specific", "adapters", @@ -25475,6 +25672,7 @@ "advertised", "after", "against", + "agent", "agents", "agents.md", "alone", @@ -25488,22 +25686,30 @@ "aoa-session-memory-global-route", "apache", "apache-2.0", + "app", + "app-server", "applicability", "applicable", "architecture", "archive", + "archived", "asks", "assure", "audit", "authored", "authored-procedure", "authority", + "authorized", + "availability", "available", "baseline", "batch", "before", "behavior", + "bind", + "binding", "bindings", + "block", "both", "boundaries", "boundary", @@ -25527,30 +25733,36 @@ "chosen", "classifier", "classify", + "cli", "codex", "codex-compact-probe", "codex-history-root", "codex-hooks-status", "command", - "commands", "compact", "compaction", "composition", "concrete", + "configuration", + "consumer", "context", "continuation", + "continuity", + "contract", "corpus", "curate", "current", "dag", "data", "decision", + "declares", "deeper", "description", "design", "design.agents.md", "design.md", "diagnostics", + "different", "discovery", "distillation", "distinct", @@ -25558,9 +25770,11 @@ "doctor", "does", "effects", + "eligibility", "entrypoint", "eval", "event", + "every", "evidence", "exact", "executable", @@ -25570,7 +25784,7 @@ "explicit", "explicitly", "export", - "failures", + "exposure", "families", "filesystem", "fingerprint", @@ -25587,6 +25801,7 @@ "global-router", "globally", "graph", + "guidance", "hand", "hand-writing", "handoff", @@ -25595,14 +25810,17 @@ "historical", "history", "home", + "home-port", "honest", "hook", "hooks", "host", "identity", "if", + "imply", "import", "incidents", + "including", "independent", "independent-callable", "index", @@ -25621,13 +25839,13 @@ "invoking", "its", "itself", - "jsonl", "judgment", "keep", "known", "layers", "level", "license", + "lifecycle", "line", "live", "live-scenario-audit", @@ -25645,6 +25863,7 @@ "matches", "material", "matters", + "mcp", "md", "memory", "memory.py", @@ -25652,6 +25871,7 @@ "metadata", "missing", "mode", + "model", "more", "name", "names", @@ -25662,6 +25882,7 @@ "naming.md", "narrower", "narrowest", + "needs", "negative", "no", "no-fit", @@ -25675,6 +25896,7 @@ "operation", "operations", "optional", + "others", "output", "owner", "owner-skill-procedure", @@ -25691,6 +25913,7 @@ "pipeline.md", "planner", "plans", + "port", "portability", "portable", "portable-source-root", @@ -25700,11 +25923,13 @@ "precompact", "preparing", "preserve", + "preserved", "prior", "prior-session", "probe", "procedure", "project", + "projection", "prompt", "prompt-visible", "proof", @@ -25717,8 +25942,10 @@ "read", "read-only", "read-owner-source", + "readable", "readiness", "readiness.md", + "reasoning", "receipt", "recipes", "references", @@ -25731,8 +25958,10 @@ "replace", "request", "required", + "requirements", "requiring", "resolve", + "response", "result", "resume", "retain", @@ -25741,6 +25970,7 @@ "retrieve", "review", "review-phase-name", + "role", "root", "roots", "route", @@ -25748,8 +25978,10 @@ "router", "routers", "routing", + "rule", "run", "runtime", + "same", "scenario", "scope", "scripts", @@ -25764,6 +25996,7 @@ "selection", "semantic", "separate", + "server", "session", "session-memory", "session-memory-request", @@ -25773,6 +26006,7 @@ "sessions", "sessionstart", "set", + "settings", "similarity", "skill", "skill.aoa-session-memory-global-route", @@ -25801,8 +26035,10 @@ "taxonomy", "than", "then", + "they", "three", "through", + "tool", "top", "top-level", "topical", @@ -31780,7 +32016,7 @@ ], "schema_version": "aoa-capability-graph-v1", "source": { - "content_hash": "f6d51e2a0f348f2444f337fa652440d931296924119cd3b503d3d25da1c3a74a", + "content_hash": "8e0659361a4ff83f8b7bbcaea696e4dadfb41b3871bdec37b81a0825fd6a9fd7", "contract": { "contract_files": [ { @@ -31805,7 +32041,7 @@ }, { "path": "scripts/skill_model/capability_system.py", - "sha256": "a6fcad55a49da845d9f227cb327997a6dab6cccda7737f261fc140644bddc5fc" + "sha256": "87e3f8f573142eb6ff241753e9e116fbf8f04d41b1e3d9c27c7ba6c82cb5c41c" }, { "path": "scripts/skill_model/capability_home_port.py", @@ -31833,7 +32069,7 @@ "family_files": [ { "path": "capabilities/families/session-memory.yaml", - "sha256": "4ec659ed875146667054f18459eb0065f273ab19d383daed22815ab2137720fc" + "sha256": "1f1827e83cc443c17008810816a94023cc3f43d941024f77d872d37fc22a3e9a" } ], "port_manifest": { @@ -31887,7 +32123,7 @@ }, { "path": "skills/aoa-session-memory-evidence-route/SKILL.md", - "sha256": "81156bda70d2a4ed2602765e1394010bba2de2f764e39c4c6ca2daa6fcd6ef74" + "sha256": "519e7c5acf57692be57a188bb227ad08c6441f02ae3e823684125de0c7c9b628" }, { "path": "skills/aoa-session-memory-evidence-route/references/consumer-profiles.md", @@ -31903,7 +32139,7 @@ }, { "path": "skills/aoa-session-memory-global-route/SKILL.md", - "sha256": "cf73f6b25c6f38afac17bc3a32be715936dc42b1b5738705d9d57d2d7647ac3b" + "sha256": "60af212b6f98d56cb0476b3479a37cddcfcce4d58de85d6231f352ceda74a688" }, { "path": "skills/aoa-session-memory-stress-pass/SKILL.md", diff --git a/generated/capability_graph.md b/generated/capability_graph.md index 03dbd8f7..4eeeafae 100644 --- a/generated/capability_graph.md +++ b/generated/capability_graph.md @@ -2,7 +2,7 @@ Derived from `capabilities/families/*.yaml`. This file is a read model, not capability authority. -Source content hash: `f6d51e2a0f348f2444f337fa652440d931296924119cd3b503d3d25da1c3a74a` +Source content hash: `8e0659361a4ff83f8b7bbcaea696e4dadfb41b3871bdec37b81a0825fd6a9fd7` ## Semantic tree diff --git a/scripts/install_skill_interface.py b/scripts/install_skill_interface.py new file mode 100644 index 00000000..60d7bf02 --- /dev/null +++ b/scripts/install_skill_interface.py @@ -0,0 +1,2434 @@ +#!/usr/bin/env python3 +"""Install the source-owned session-memory skill interface as a bounded overlay. + +The normal session-memory installer owns the portable kernel and its runtime +stores. This module owns a smaller, independently reversible component: the +capability home read models and selected skill packages. It deliberately does +not call ``copy_portable_bundle``. The component is admitted only when the +source is a clean Git checkout, the caller names the aoa-skills contract +checkout explicitly, and the target has a valid unchanged kernel install +profile. + +The command line has three verbs: + +``check`` + Perform the complete read-only preflight and print a JSON plan. +``install`` / ``execute`` + Apply the plan after ``--force`` has explicitly authorized replacing a + selected component. +``rollback`` + Verify the current after-state and restore the selected paths and the + previous component receipt from the durable backup. + +The receipt and backup are runtime diagnostics. They are not part of the +portable source and do not confer prompt selection, invocation, routing +quality, runtime health, or outcome evidence. +""" + +from __future__ import annotations + +import argparse +import base64 +from dataclasses import dataclass +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import shutil +import stat +import subprocess +import sys +import tempfile +import time +from typing import Any, Callable, Iterable, Mapping, Sequence +import uuid + + +# ``scripts`` is on sys.path when this file is run directly. The fallback is +# useful for callers importing this module from an arbitrary working directory. +try: + import aoa_session_memory as _session_memory +except ModuleNotFoundError: # pragma: no cover - exercised by direct importers + _SCRIPT_DIR = Path(__file__).resolve().parent + if str(_SCRIPT_DIR) not in sys.path: + sys.path.insert(0, str(_SCRIPT_DIR)) + import aoa_session_memory as _session_memory + + +runtime_install_source_provenance = ( + _session_memory.runtime_install_source_provenance +) +runtime_install_profile_status = _session_memory.runtime_install_profile_status + + +SCHEMA_VERSION = "aoa_session_memory_skill_projection_v1" +RECEIPT_RELATIVE = Path("diagnostics/skill-projection-install.json") +ROLLBACK_OUTCOME_RELATIVE = Path("diagnostics/skill-projection-rollback.json") +BACKUP_PARENT_RELATIVE = Path("diagnostics/skill-projection-backups") +BACKUP_MANIFEST_NAME = "backup-manifest.json" +PREVIOUS_RECEIPT_NAME = "previous-receipt.bin" +PREVIOUS_RECEIPT_STATE_NAME = "previous-receipt.json" +STAGE_PREFIX = ".skill-projection-stage-" +SOURCE_SKIP_RELATIVE = Path("scripts/install_skill_interface.py") + +# This is the fixed interface closure. The two package names are also the +# only default prompt-visible routers; additional names must be graph-declared +# and are explicit ``--skill`` values. +FIXED_SOURCE_RELATIVE = ( + Path("capabilities/AGENTS.md"), + Path("capabilities/port.manifest.json"), + Path("capabilities/families/session-memory.yaml"), + Path("generated/capability_graph.json"), + Path("generated/capability_graph.md"), + Path("skills/AGENTS.md"), + Path("skills/port.manifest.json"), +) +DEFAULT_SKILLS = ( + "aoa-session-memory-global-route", + "aoa-session-memory-evidence-route", +) +GLOBAL_ROUTER_RELATIVE = Path( + "skills/aoa-session-memory-global-route/references/capability-router.md" +) +SKILL_NAME_RE = r"[A-Za-z0-9][A-Za-z0-9._-]*" + +# ``copy_portable_bundle`` renders this example for the selected runtime and +# preserves these generated map artifacts. They are runtime-owned overlays, +# so an old value does not constitute unselected source drift. +RUNTIME_GENERATED_RELATIVES = { + Path("hooks/codex-hooks.user.example.json"), + Path("maps/INDEX.md"), + Path("maps/index.json"), + Path("maps/index-state.json"), + Path("maps/entity-registry.json"), + Path("maps/entity-registry.md"), +} + + +class SkillProjectionError(ValueError): + """Expected preflight, compare-and-swap, or rollback failure.""" + + +class PreflightError(SkillProjectionError): + def __init__(self, diagnostics: Sequence[str], payload: Mapping[str, Any] | None = None): + self.diagnostics = list(dict.fromkeys(str(item) for item in diagnostics)) + self.payload = dict(payload or {}) + super().__init__("; ".join(self.diagnostics) or "skill projection preflight failed") + + +@dataclass(frozen=True) +class SourcePackage: + name: str + root: Path + version: str + fingerprint: str + graph_node_id: str + declared_files: tuple[dict[str, Any], ...] + + +@dataclass(frozen=True) +class Plan: + source_root: Path + skills_root: Path + workspace_root: Path + target_root: Path + source_identity: dict[str, Any] + owner_validation: dict[str, Any] + base_profile: dict[str, Any] + source_metadata: dict[str, Any] + selected_skills: tuple[str, ...] + selected_roots: tuple[Path, ...] + source_roots: tuple[dict[str, Any], ...] + target_roots: tuple[dict[str, Any], ...] + source_files: dict[str, dict[str, Any]] + target_files: dict[str, dict[str, Any]] + unselected_token: str + source_token: str + target_cas_token: str + previous_receipt: dict[str, Any] + + +def _utc_now() -> str: + return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace( + "+00:00", "Z" + ) + + +def _sha256_bytes(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest() + + +def _sha256_file(path: Path) -> str: + return _sha256_bytes(path.read_bytes()) + + +def _canonical_json(value: Any) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def _sha256_json(value: Any) -> str: + return _sha256_bytes(_canonical_json(value)) + + +def _mode(path: Path) -> int: + return stat.S_IMODE(path.stat().st_mode) + + +def _mode_text(value: int | None) -> str | None: + return None if value is None else format(int(value), "04o") + + +def _resolve(path: Path | str) -> Path: + return Path(path).expanduser().resolve(strict=False) + + +def _safe_relative(value: Path | str, *, label: str) -> Path: + path = Path(value) + if path.is_absolute() or not path.parts or ".." in path.parts: + raise SkillProjectionError(f"{label} must be a non-empty safe relative path: {value!r}") + if any(part in {"", "."} for part in path.parts): + raise SkillProjectionError(f"{label} contains an unsupported path component: {value!r}") + return path + + +def _root_ready(path: Path, *, label: str) -> Path: + lexical = Path(path).expanduser() + if lexical.is_symlink(): + raise SkillProjectionError(f"{label} must not be a symlink: {lexical}") + root = _resolve(lexical) + if root.is_symlink(): + raise SkillProjectionError(f"{label} must not be a symlink: {root}") + if not root.exists() or not root.is_dir(): + raise SkillProjectionError(f"{label} directory is missing: {root}") + return root + + +def _path_inside(root: Path, relative: Path, *, label: str) -> Path: + rel = _safe_relative(relative, label=label) + candidate = root.joinpath(rel) + current = root + for part in rel.parts: + current = current / part + if current.is_symlink(): + raise SkillProjectionError( + f"{label} path component must not be a symlink: {rel.as_posix()}" + ) + return candidate + + +def _record(path: Path, *, state: str = "file") -> dict[str, Any]: + if path.is_symlink(): + raise SkillProjectionError(f"path must be a regular non-symlink file: {path}") + if not path.exists() or not path.is_file(): + raise SkillProjectionError(f"path is not a regular file: {path}") + raw = path.read_bytes() + mode = _mode(path) + return { + "state": state, + "sha256": _sha256_bytes(raw), + "bytes": len(raw), + "mode": mode, + "mode_text": _mode_text(mode), + "executable": bool(mode & 0o111), + } + + +def _absent_record() -> dict[str, Any]: + return { + "state": "absent", + "sha256": None, + "bytes": 0, + "mode": None, + "mode_text": None, + } + + +def _snapshot_tree(path: Path, *, relative_root: Path) -> dict[str, Any]: + """Snapshot a selected file or directory, retaining all pre-state files.""" + if path.is_symlink(): + raise SkillProjectionError( + f"target/source selected path must not be a symlink: {relative_root.as_posix()}" + ) + if not path.exists(): + return { + "path": relative_root.as_posix(), + "state": "absent", + "mode": None, + "mode_text": None, + "files": [], + } + if path.is_file(): + item = _record(path) + item["path"] = relative_root.as_posix() + return { + "path": relative_root.as_posix(), + "state": "file", + "mode": item["mode"], + "mode_text": item["mode_text"], + "files": [item], + } + if not path.is_dir(): + raise SkillProjectionError( + f"selected path must be a regular file or directory: {relative_root.as_posix()}" + ) + files: list[dict[str, Any]] = [] + for child in sorted(path.rglob("*"), key=lambda item: item.as_posix()): + child_rel = child.relative_to(path) + if child.is_symlink(): + raise SkillProjectionError( + f"selected path contains a symlink: " + f"{relative_root.joinpath(child_rel).as_posix()}" + ) + if child.is_dir(): + continue + if not child.is_file(): + raise SkillProjectionError( + f"selected path contains a non-regular entry: " + f"{relative_root.joinpath(child_rel).as_posix()}" + ) + item = _record(child) + item["path"] = relative_root.joinpath(child_rel).as_posix() + files.append(item) + root_mode = _mode(path) + return { + "path": relative_root.as_posix(), + "state": "directory", + "mode": root_mode, + "mode_text": _mode_text(root_mode), + "files": files, + } + + +def _flatten_roots(roots: Iterable[Mapping[str, Any]]) -> dict[str, dict[str, Any]]: + result: dict[str, dict[str, Any]] = {} + for root in roots: + for item in root.get("files", []): + if isinstance(item, Mapping) and isinstance(item.get("path"), str): + result[str(item["path"])] = dict(item) + return result + + +def _root_snapshots_equal(left: Mapping[str, Any], right: Mapping[str, Any]) -> bool: + if left.get("path") != right.get("path") or left.get("state") != right.get("state"): + return False + if left.get("mode") != right.get("mode"): + return False + left_files = left.get("files") if isinstance(left.get("files"), list) else [] + right_files = right.get("files") if isinstance(right.get("files"), list) else [] + def normalized(items: list[Any]) -> list[dict[str, Any]]: + return [ + { + "path": str(item.get("path")), + "state": str(item.get("state")), + "sha256": item.get("sha256"), + "bytes": int(item.get("bytes") or 0), + "mode": item.get("mode"), + } + for item in items + if isinstance(item, Mapping) + ] + return normalized(left_files) == normalized(right_files) + + +def _snapshot_roots(root: Path, relatives: Sequence[Path]) -> tuple[dict[str, Any], ...]: + return tuple( + _snapshot_tree( + _path_inside(root, relative, label="selected target path"), + relative_root=relative, + ) + for relative in relatives + ) + + +def _remove_path(path: Path) -> None: + if not path.exists() and not path.is_symlink(): + return + if path.is_symlink() or path.is_file(): + path.unlink() + return + if path.is_dir(): + shutil.rmtree(path) + return + path.unlink() + + +def _fsync_file(path: Path) -> None: + with path.open("rb") as handle: + os.fsync(handle.fileno()) + + +def _fsync_dir(path: Path) -> None: + try: + fd = os.open(str(path), os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) + except OSError: + return + try: + os.fsync(fd) + finally: + os.close(fd) + + +def _atomic_write(path: Path, raw: bytes, *, prefix: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + fd, name = tempfile.mkstemp(prefix=prefix, suffix=".tmp", dir=str(path.parent)) + temp = Path(name) + try: + with os.fdopen(fd, "wb") as handle: + handle.write(raw) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temp, path) + _fsync_dir(path.parent) + finally: + if temp.exists() or temp.is_symlink(): + temp.unlink() + + +def _load_json(path: Path, *, label: str) -> Any: + if path.is_symlink() or not path.is_file(): + raise SkillProjectionError(f"{label} must be a regular file: {path}") + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise SkillProjectionError(f"{label} is unreadable or invalid JSON: {path}") from exc + + +def _graph_skill_file_records( + source_root: Path, + graph: Mapping[str, Any], + *, + projection_paths: set[str], +) -> tuple[dict[str, dict[str, Any]], dict[str, Path], list[str]]: + """Read only the graph-declared skill package closure. + + The runtime kernel is intentionally outside this comparison. The owner + validator already checks the complete source graph; this function supplies + the narrow source/target parity boundary for graph-declared skill packages + that the overlay leaves untouched. + """ + nodes = graph.get("nodes") + if not isinstance(nodes, list): + raise SkillProjectionError("capability graph nodes are missing") + records: dict[str, dict[str, Any]] = {} + package_roots: dict[str, Path] = {} + diagnostics: list[str] = [] + for node in nodes: + if not isinstance(node, Mapping) or node.get("kind") != "skill": + continue + node_id = str(node.get("id") or "") + if not node_id.startswith("skill."): + continue + name = node_id.removeprefix("skill.") + _safe_skill_name(name) + binding = node.get("binding") + package = node.get("package") + if not isinstance(binding, Mapping) or not isinstance(package, Mapping): + raise SkillProjectionError(f"graph skill package metadata is incomplete: {name}") + binding_ref = _safe_relative( + str(binding.get("ref") or ""), + label="graph skill binding", + ) + if binding_ref != Path("skills") / name / "SKILL.md": + raise SkillProjectionError(f"graph skill binding escapes package: {name}") + package_root = Path("skills") / name + package_roots[name] = package_root + declared = package.get("files") + if not isinstance(declared, list) or not declared: + raise SkillProjectionError(f"graph skill package files are missing: {name}") + declared_paths: set[str] = set() + for row in declared: + if not isinstance(row, Mapping): + raise SkillProjectionError(f"graph skill package row is invalid: {name}") + relative = _safe_relative(str(row.get("path") or ""), label="graph package file") + if not relative.as_posix().startswith(package_root.as_posix() + "/"): + raise SkillProjectionError(f"graph package file escapes package: {relative.as_posix()}") + relative_text = relative.as_posix() + declared_paths.add(relative_text) + path = _path_inside(source_root, relative, label="graph package source file") + observed = _record(path) + if any( + row.get(key) != observed[key] + for key in ("sha256", "bytes", "executable") + ): + raise SkillProjectionError( + f"graph package source metadata mismatch: {relative_text}" + ) + if relative_text not in projection_paths: + records[relative_text] = observed + root_path = _path_inside(source_root, package_root, label="graph skill package root") + if root_path.is_symlink() or not root_path.is_dir(): + raise SkillProjectionError(f"graph skill package root is missing or symlinked: {name}") + actual_paths: set[str] = set() + for child in sorted(root_path.rglob("*"), key=lambda value: value.as_posix()): + relative_text = child.relative_to(source_root).as_posix() + if child.is_symlink(): + raise SkillProjectionError(f"graph skill package contains symlink: {relative_text}") + if child.is_dir(): + continue + if not child.is_file(): + raise SkillProjectionError(f"graph skill package contains non-regular entry: {relative_text}") + if relative_text not in projection_paths: + actual_paths.add(relative_text) + extra = sorted(actual_paths - declared_paths) + missing = sorted(declared_paths - actual_paths - projection_paths) + diagnostics.extend(f"graph_package_source_extra:{item}" for item in extra) + diagnostics.extend(f"graph_package_source_missing:{item}" for item in missing) + if diagnostics: + raise SkillProjectionError("; ".join(diagnostics)) + return records, package_roots, [] + + +def _target_record(root: Path, relative: Path) -> dict[str, Any]: + path = _path_inside(root, relative, label="unselected target path") + if path.is_symlink(): + return {"state": "symlink", "sha256": None, "bytes": 0, "mode": None} + if not path.exists(): + return _absent_record() + if not path.is_file(): + return {"state": "non-regular", "sha256": None, "bytes": 0, "mode": None} + return _record(path) + + +def _unselected_parity( + source_root: Path, + target_root: Path, + *, + graph: Mapping[str, Any], + projection_paths: set[str], + selected_skills: set[str], + selected_files: set[str], +) -> tuple[list[str], dict[str, dict[str, Any]], str]: + all_source_records, package_roots, _ = _graph_skill_file_records( + source_root, + graph, + projection_paths=projection_paths, + ) + source_records = { + relative: record + for relative, record in all_source_records.items() + if relative not in selected_files + and not any( + relative == root.as_posix() or relative.startswith(root.as_posix() + "/") + for name, root in package_roots.items() + if name in selected_skills + ) + } + diagnostics: list[str] = [] + target_records: dict[str, dict[str, Any]] = {} + for relative_text, source_record in sorted(source_records.items()): + relative = Path(relative_text) + target_record = _target_record(target_root, relative) + target_records[relative_text] = target_record + if target_record.get("state") != "file": + diagnostics.append(f"unselected_target_mismatch:{relative_text}") + continue + if any( + target_record.get(key) != source_record.get(key) + for key in ("sha256", "bytes", "mode") + ): + diagnostics.append(f"unselected_target_mismatch:{relative_text}") + extras: list[str] = [] + for name, package_root in sorted(package_roots.items()): + if name in selected_skills: + continue + target_package_root = _path_inside( + target_root, + package_root, + label="unselected target skill package", + ) + if target_package_root.is_symlink(): + extras.append(f"{package_root.as_posix()}:symlink") + continue + if not target_package_root.exists(): + continue + if not target_package_root.is_dir(): + extras.append(f"{package_root.as_posix()}:non-directory") + continue + for child in sorted(target_package_root.rglob("*"), key=lambda value: value.as_posix()): + relative = child.relative_to(target_root) + relative_text = relative.as_posix() + if child.is_symlink(): + if relative_text not in selected_files: + extras.append(f"{relative_text}:symlink") + continue + if child.is_dir(): + continue + if not child.is_file(): + extras.append(f"{relative_text}:non-regular") + continue + if relative_text in selected_files: + continue + if relative_text not in all_source_records: + extras.append(relative_text) + diagnostics.extend(f"unselected_target_extra:{item}" for item in extras) + token_rows = [ + { + "path": relative, + "source": dict(source_records[relative]), + "target": dict(target_records.get(relative) or _absent_record()), + } + for relative in sorted(source_records) + ] + token_rows.extend({"path": item, "extra": True} for item in extras) + return diagnostics, source_records, _sha256_json(token_rows) + + +def _safe_skill_name(name: str) -> str: + import re + + if not re.fullmatch(SKILL_NAME_RE, name): + raise SkillProjectionError(f"skill name is not a safe graph package name: {name!r}") + return name + + +def _contract_file_rows(skills_root: Path, graph: Mapping[str, Any]) -> dict[str, Any]: + source = graph.get("source") if isinstance(graph.get("source"), Mapping) else {} + contract = source.get("contract") if isinstance(source.get("contract"), Mapping) else {} + if not contract: + raise SkillProjectionError("capability graph has no shared contract metadata") + rows = contract.get("contract_files") + if not isinstance(rows, list): + raise SkillProjectionError("capability graph shared contract files are missing") + owner_repo = str(contract.get("owner_repo") or "") + schema_path = str(contract.get("schema_path") or "") + schema_sha256 = str(contract.get("schema_sha256") or "") + validator_path_text = str( + contract.get("validator_path") + or "scripts/validation/validate_capability_home_port.py" + ) + validator_sha256 = str(contract.get("validator_sha256") or "") + if not owner_repo or not schema_path or not schema_sha256 or not validator_sha256: + raise SkillProjectionError("capability graph shared contract identity is incomplete") + checked: list[dict[str, Any]] = [] + for row in rows: + if not isinstance(row, Mapping): + raise SkillProjectionError("capability graph shared contract row is invalid") + relative = _safe_relative(str(row.get("path") or ""), label="shared contract path") + expected = str(row.get("sha256") or "") + path = _path_inside(skills_root, relative, label="shared contract path") + if path.is_symlink() or not path.is_file(): + raise SkillProjectionError(f"shared contract file is missing: {relative.as_posix()}") + observed = _sha256_file(path) + if expected and observed != expected: + raise SkillProjectionError( + f"shared contract digest mismatch: {relative.as_posix()}" + ) + checked.append({"path": relative.as_posix(), "sha256": observed}) + # The graph records the implementation validator under ``scripts/validation`` + # as part of the shared contract. The owner-facing entrypoint is the + # repository wrapper, which supplies the package import path and is the + # stable command used by callers. + contract_validator_path = _safe_relative( + validator_path_text, + label="shared validator path", + ) + contract_validator = _path_inside( + skills_root, + contract_validator_path, + label="shared validator path", + ) + validator_path = Path("scripts/validate_capability_home_port.py") + validator = _path_inside(skills_root, validator_path, label="owner validator entrypoint") + if contract_validator.is_symlink() or not contract_validator.is_file(): + raise SkillProjectionError( + f"shared validator implementation is missing: {contract_validator_path.as_posix()}" + ) + if validator.is_symlink() or not validator.is_file(): + raise SkillProjectionError(f"owner validator entrypoint is missing: {validator_path.as_posix()}") + checked_by_path = {str(row["path"]): str(row["sha256"]) for row in checked} + if checked_by_path.get(schema_path) != schema_sha256: + raise SkillProjectionError("shared contract schema digest does not match its file") + if checked_by_path.get(contract_validator_path.as_posix()) != validator_sha256: + raise SkillProjectionError("shared contract validator digest does not match its file") + return { + "owner_repo": owner_repo, + "schema_path": schema_path, + "schema_sha256": schema_sha256, + "validator_path": validator_path.as_posix(), + "validator_sha256": _sha256_file(contract_validator), + "validator_entrypoint_sha256": _sha256_file(validator), + "contract_validator_path": contract_validator_path.as_posix(), + "contract_files": checked, + "digest": "sha256:" + _sha256_json(contract), + } + + +def _run_owner_validation(skills_root: Path, source_root: Path) -> dict[str, Any]: + """Run the explicitly selected aoa-skills owner validator.""" + graph_path = source_root / "generated/capability_graph.json" + graph = _load_json(graph_path, label="source capability graph") + shared_contract = _contract_file_rows(skills_root, graph) + validator = skills_root / shared_contract["validator_path"] + command = [ + sys.executable, + str(validator), + "--owner-root", + str(source_root), + "--check-generated", + ] + try: + environment = os.environ.copy() + scripts_path = str(skills_root / "scripts") + existing_pythonpath = environment.get("PYTHONPATH") + environment["PYTHONPATH"] = ( + scripts_path + if not existing_pythonpath + else f"{scripts_path}{os.pathsep}{existing_pythonpath}" + ) + completed = subprocess.run( + command, + cwd=str(skills_root), + env=environment, + capture_output=True, + text=True, + check=False, + ) + except OSError as exc: + return { + "ok": False, + "status": "validator_unavailable", + "command": command, + "returncode": None, + "stdout": "", + "stderr": str(exc), + "shared_contract": shared_contract, + } + return { + "ok": completed.returncode == 0, + "status": "validated" if completed.returncode == 0 else "validator_failed", + "command": command, + "returncode": completed.returncode, + "stdout": (completed.stdout or "")[-4000:], + "stderr": (completed.stderr or "")[-4000:], + "shared_contract": shared_contract, + } + + +def _load_source_metadata( + source_root: Path, + *, + selected_skills: Sequence[str], +) -> tuple[dict[str, Any], tuple[SourcePackage, ...], tuple[Path, ...]]: + capabilities_manifest_path = source_root / "capabilities/port.manifest.json" + skills_manifest_path = source_root / "skills/port.manifest.json" + graph_path = source_root / "generated/capability_graph.json" + graph_markdown_path = source_root / "generated/capability_graph.md" + capabilities_manifest = _load_json( + capabilities_manifest_path, + label="source capabilities manifest", + ) + skills_manifest = _load_json(skills_manifest_path, label="source skills manifest") + graph = _load_json(graph_path, label="source capability graph") + if not isinstance(capabilities_manifest, Mapping): + raise SkillProjectionError("source capabilities manifest is not an object") + if not isinstance(skills_manifest, Mapping): + raise SkillProjectionError("source skills manifest is not an object") + if not isinstance(graph, Mapping): + raise SkillProjectionError("source capability graph is not an object") + graph_source = graph.get("source") + if not isinstance(graph_source, Mapping) or not str(graph_source.get("content_hash") or ""): + raise SkillProjectionError("source capability graph content hash is missing") + projection = capabilities_manifest.get("projection") + if not isinstance(projection, Mapping): + raise SkillProjectionError("source capabilities manifest projection is missing") + router_text = str(projection.get("router_markdown") or "") + if router_text != GLOBAL_ROUTER_RELATIVE.as_posix(): + raise SkillProjectionError("source global router projection path is not canonical") + graph_json_text = str(projection.get("graph_json") or "") + graph_md_text = str(projection.get("graph_markdown") or "") + if graph_json_text != "generated/capability_graph.json" or graph_md_text != "generated/capability_graph.md": + raise SkillProjectionError("source capability projection paths are not canonical") + + bundles = skills_manifest.get("bundles") + if not isinstance(bundles, list): + raise SkillProjectionError("source skills manifest bundles are missing") + bundle_by_name = { + str(bundle.get("name")): bundle + for bundle in bundles + if isinstance(bundle, Mapping) and bundle.get("name") + } + nodes = graph.get("nodes") + if not isinstance(nodes, list): + raise SkillProjectionError("source capability graph nodes are missing") + node_by_id = { + str(node.get("id")): node + for node in nodes + if isinstance(node, Mapping) and node.get("id") + } + package_specs: list[SourcePackage] = [] + package_roots: list[Path] = [] + selected_files = { + relative.as_posix() for relative in FIXED_SOURCE_RELATIVE + } + selected_root_paths: list[Path] = list(FIXED_SOURCE_RELATIVE) + package_metadata: list[dict[str, Any]] = [] + projection_paths = { + str(projection.get("router_markdown")), + str(projection.get("graph_json")), + str(projection.get("graph_markdown")), + } + for name in selected_skills: + safe_name = _safe_skill_name(str(name)) + node_id = f"skill.{safe_name}" + node = node_by_id.get(node_id) + if not isinstance(node, Mapping) or node.get("kind") != "skill": + raise SkillProjectionError(f"selected skill is not graph-declared: {safe_name}") + lifecycle = node.get("lifecycle") + package = node.get("package") + binding = node.get("binding") + if not isinstance(lifecycle, Mapping) or not isinstance(package, Mapping) or not isinstance(binding, Mapping): + raise SkillProjectionError(f"selected graph package metadata is incomplete: {safe_name}") + version = str(lifecycle.get("version") or package.get("version") or "") + fingerprint = str(package.get("fingerprint") or "") + binding_ref = str(binding.get("ref") or "") + expected_root = Path(f"skills/{safe_name}") + if binding_ref != expected_root.joinpath("SKILL.md").as_posix(): + raise SkillProjectionError(f"selected skill binding is outside its package: {safe_name}") + source_package_root = _path_inside(source_root, expected_root, label="source skill package") + if source_package_root.is_symlink() or not source_package_root.is_dir(): + raise SkillProjectionError(f"selected skill package is missing or symlinked: {safe_name}") + declared_files = package.get("files") + if not isinstance(declared_files, list) or not declared_files: + raise SkillProjectionError(f"selected skill package files are missing: {safe_name}") + actual_rows: list[dict[str, Any]] = [] + actual_all: list[dict[str, Any]] = [] + for child in sorted(source_package_root.rglob("*"), key=lambda value: value.as_posix()): + child_rel = child.relative_to(source_root).as_posix() + if child.is_symlink(): + raise SkillProjectionError(f"selected skill package contains symlink: {child_rel}") + if child.is_dir(): + continue + if not child.is_file(): + raise SkillProjectionError(f"selected skill package contains non-regular entry: {child_rel}") + row = { + "path": child_rel, + "sha256": _sha256_file(child), + "bytes": child.stat().st_size, + "executable": bool(child.stat().st_mode & 0o111), + "mode": _mode(child), + } + actual_all.append(row) + if child_rel not in projection_paths: + actual_rows.append(row) + declared_normalized: list[dict[str, Any]] = [] + for row in declared_files: + if not isinstance(row, Mapping): + raise SkillProjectionError(f"selected package row is invalid: {safe_name}") + row_path = _safe_relative(str(row.get("path") or ""), label="selected package file") + if not row_path.as_posix().startswith(expected_root.as_posix() + "/"): + raise SkillProjectionError(f"selected package file escapes package root: {row_path.as_posix()}") + path = _path_inside(source_root, row_path, label="selected package file") + if path.is_symlink() or not path.is_file(): + raise SkillProjectionError(f"selected package file is missing: {row_path.as_posix()}") + observed = { + "path": row_path.as_posix(), + "sha256": _sha256_file(path), + "bytes": path.stat().st_size, + "executable": bool(path.stat().st_mode & 0o111), + "mode": _mode(path), + } + for key in ("sha256", "bytes", "executable"): + if row.get(key) != observed[key]: + raise SkillProjectionError( + f"selected package metadata mismatch: {safe_name}:{row_path.as_posix()}" + ) + declared_normalized.append(observed) + actual_identity = [ + { + "path": row["path"], + "sha256": row["sha256"], + "executable": row["executable"], + } + for row in actual_rows + ] + declared_identity = [ + { + "path": row["path"], + "sha256": row["sha256"], + "executable": row["executable"], + } + for row in declared_normalized + ] + if sorted(actual_identity, key=lambda item: item["path"]) != sorted( + declared_identity, key=lambda item: item["path"] + ): + raise SkillProjectionError(f"selected package closure differs from graph: {safe_name}") + observed_fingerprint = _sha256_json( + sorted(actual_identity, key=lambda item: item["path"]) + ) + if fingerprint != observed_fingerprint: + raise SkillProjectionError(f"selected package fingerprint mismatch: {safe_name}") + bundle = bundle_by_name.get(safe_name) + if isinstance(bundle, Mapping): + if str(bundle.get("path") or "") != expected_root.as_posix(): + raise SkillProjectionError(f"selected skill manifest path mismatch: {safe_name}") + if str(bundle.get("version") or "") != version: + raise SkillProjectionError(f"selected skill manifest version mismatch: {safe_name}") + package_specs.append( + SourcePackage( + name=safe_name, + root=expected_root, + version=version, + fingerprint=fingerprint, + graph_node_id=node_id, + declared_files=tuple(declared_normalized), + ) + ) + package_roots.append(expected_root) + selected_root_paths.append(expected_root) + selected_files.update(row["path"] for row in actual_all) + package_metadata.append( + { + "name": safe_name, + "path": expected_root.as_posix(), + "version": version, + "fingerprint": fingerprint, + "graph_node_id": node_id, + "declared_files": declared_normalized, + } + ) + # The generated router card remains part of the fixed interface even when + # a caller explicitly selects only the evidence route. + if GLOBAL_ROUTER_RELATIVE.as_posix() not in selected_files: + selected_files.add(GLOBAL_ROUTER_RELATIVE.as_posix()) + selected_root_paths.append(GLOBAL_ROUTER_RELATIVE) + for relative in FIXED_SOURCE_RELATIVE: + path = _path_inside(source_root, relative, label="fixed source closure") + if path.is_symlink() or not path.is_file(): + raise SkillProjectionError(f"fixed source closure file is missing: {relative.as_posix()}") + router_path = _path_inside(source_root, GLOBAL_ROUTER_RELATIVE, label="generated global router") + if router_path.is_symlink() or not router_path.is_file(): + raise SkillProjectionError("generated global router card is missing") + source_files: dict[str, dict[str, Any]] = {} + for relative in sorted(selected_files): + path = _path_inside(source_root, Path(relative), label="selected source file") + source_files[relative] = _record(path) + metadata = { + "capabilities_manifest": capabilities_manifest, + "skills_manifest": skills_manifest, + "graph": graph, + "graph_source_content_hash": str(graph_source.get("content_hash")), + "graph_projection_paths": sorted(projection_paths), + "graph_json_sha256": _sha256_file(graph_path), + "graph_markdown_sha256": _sha256_file(graph_markdown_path), + "router_sha256": _sha256_file(router_path), + "packages": package_metadata, + "selected_files": source_files, + "selected_roots": [path.as_posix() for path in selected_root_paths], + "shared_contract": None, + } + return metadata, tuple(package_specs), tuple(dict.fromkeys(selected_root_paths)) + + +def _base_profile(target_root: Path, workspace_root: Path) -> dict[str, Any]: + status = runtime_install_profile_status( + root=target_root, + workspace_root=workspace_root, + ) + if not status.get("valid"): + raise SkillProjectionError( + "invalid runtime install profile: " + + ", ".join(str(item) for item in status.get("diagnostics", [])) + ) + path = _path_inside(target_root, _session_memory.INSTALL_PROFILE_PATH, label="base install profile") + raw = path.read_bytes() + try: + payload = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise SkillProjectionError("base runtime install profile JSON is invalid") from exc + if not isinstance(payload, Mapping): + raise SkillProjectionError("base runtime install profile is not an object") + return { + "path": _session_memory.INSTALL_PROFILE_PATH.as_posix(), + "sha256": _sha256_bytes(raw), + "bytes": len(raw), + "install_id": payload.get("install_id"), + "source_ref": payload.get("source_ref"), + "source_root": payload.get("source_root"), + "source_commit": payload.get("source_commit"), + "source_tree": payload.get("source_tree"), + "source_script": payload.get("source_script"), + "source_script_sha256": payload.get("source_script_sha256"), + "source_worktree_clean": payload.get("source_worktree_clean"), + "include_tests": payload.get("include_tests"), + "raw_b64": base64.b64encode(raw).decode("ascii"), + "status": status, + } + + +def _previous_receipt(target_root: Path) -> dict[str, Any]: + path = _path_inside(target_root, RECEIPT_RELATIVE, label="component receipt") + if path.is_symlink(): + raise SkillProjectionError("component receipt must be a regular non-symlink file") + if not path.exists(): + return { + "present": False, + "sha256": None, + "bytes": 0, + "raw_b64": None, + } + if not path.is_file(): + raise SkillProjectionError("component receipt must be a regular file") + raw = path.read_bytes() + return { + "present": True, + "sha256": _sha256_bytes(raw), + "bytes": len(raw), + "raw_b64": base64.b64encode(raw).decode("ascii"), + } + + +def _source_identity_token(source_identity: Mapping[str, Any], selected_files: Mapping[str, Any]) -> str: + return _sha256_json( + { + "identity": { + key: source_identity.get(key) + for key in ( + "source_commit", + "source_tree", + "source_script_sha256", + "source_worktree_clean", + ) + }, + "selected_files": selected_files, + } + ) + + +def _target_cas_token( + plan: Plan, + *, + source_files: Mapping[str, Mapping[str, Any]] | None = None, +) -> str: + target_roots = _snapshot_roots(plan.target_root, plan.selected_roots) + current_profile_path = plan.target_root / _session_memory.INSTALL_PROFILE_PATH + profile_raw = current_profile_path.read_bytes() if current_profile_path.is_file() else b"" + current_receipt = _previous_receipt(plan.target_root) + _diagnostics, _records, unselected_token = _unselected_parity( + plan.source_root, + plan.target_root, + graph=plan.source_metadata["graph"], + projection_paths=set(plan.source_metadata.get("graph_projection_paths", [])), + selected_skills=set(plan.selected_skills), + selected_files=set((source_files or plan.source_metadata["selected_files"]).keys()), + ) + return _sha256_json( + { + "roots": list(target_roots), + "profile_sha256": _sha256_bytes(profile_raw), + "receipt": current_receipt, + "unselected_token": unselected_token, + } + ) + + +def _preflight( + *, + source_aoa_root: Path | str, + skills_root: Path | str, + workspace_root: Path | str, + aoa_root: Path | str, + selected_skills: Sequence[str] | None, +) -> Plan: + diagnostics: list[str] = [] + try: + source_root = _root_ready(Path(source_aoa_root), label="source .aoa root") + explicit_skills_root = _root_ready(Path(skills_root), label="aoa-skills contract root") + workspace = _root_ready(Path(workspace_root), label="workspace root") + target = _root_ready(Path(aoa_root), label="target .aoa root") + except SkillProjectionError as exc: + raise PreflightError([str(exc)]) from exc + if source_root == target: + diagnostics.append("source and target .aoa roots must differ") + names = tuple(dict.fromkeys(str(item) for item in (selected_skills or DEFAULT_SKILLS))) + if not names: + diagnostics.append("at least one graph-declared skill must be selected") + try: + source_identity = runtime_install_source_provenance(source_root) + except Exception as exc: # source helper has a broad subprocess/filesystem surface + source_identity = { + "status": "unresolved", + "identity_status": "unresolved", + "diagnostics": [f"source provenance helper failed: {exc}"], + } + if source_identity.get("status") != "current": + diagnostics.extend( + str(item) for item in source_identity.get("diagnostics", []) + if str(item) != "source_worktree_dirty" + ) + if not bool(source_identity.get("source_worktree_clean")): + diagnostics.append("source_worktree_dirty") + try: + metadata, _packages, selected_roots = _load_source_metadata( + source_root, + selected_skills=names, + ) + selected_files = dict(metadata["selected_files"]) + except SkillProjectionError as exc: + diagnostics.append(str(exc)) + metadata = { + "selected_files": {}, + "selected_roots": [], + "packages": [], + "graph_source_content_hash": "", + } + selected_roots = tuple() + selected_files = {} + try: + owner_validation = _run_owner_validation(explicit_skills_root, source_root) + metadata["shared_contract"] = owner_validation.get("shared_contract") + if not owner_validation.get("ok"): + diagnostics.append("aoa-skills owner validation failed") + except SkillProjectionError as exc: + owner_validation = { + "ok": False, + "status": "validator_preflight_failed", + "diagnostics": [str(exc)], + } + diagnostics.append(str(exc)) + try: + base_profile = _base_profile(target, workspace) + except SkillProjectionError as exc: + diagnostics.append(str(exc)) + base_profile = { + "include_tests": True, + "sha256": None, + "install_id": None, + "status": {"valid": False}, + } + if selected_roots: + try: + source_roots = _snapshot_roots(source_root, selected_roots) + target_roots = _snapshot_roots(target, selected_roots) + except SkillProjectionError as exc: + diagnostics.append(str(exc)) + source_roots = tuple() + target_roots = tuple() + else: + source_roots = tuple() + target_roots = tuple() + target_files = _flatten_roots(target_roots) + if selected_roots and selected_files: + try: + parity_diagnostics, _all_source_records, unselected_token = _unselected_parity( + source_root, + target, + graph=metadata["graph"], + projection_paths=set(metadata.get("graph_projection_paths", [])), + selected_skills=set(names), + selected_files=set(selected_files), + ) + diagnostics.extend(parity_diagnostics) + except SkillProjectionError as exc: + diagnostics.append(str(exc)) + unselected_token = "" + else: + unselected_token = "" + try: + previous_receipt = _previous_receipt(target) + except SkillProjectionError as exc: + diagnostics.append(str(exc)) + previous_receipt = {"present": False, "sha256": None, "bytes": 0, "raw_b64": None} + source_token = _source_identity_token(source_identity, selected_files) + target_cas_token = _sha256_json( + { + "roots": list(target_roots), + "profile_sha256": base_profile.get("sha256"), + "receipt": previous_receipt, + "unselected_token": unselected_token, + } + ) + if diagnostics: + payload = { + "source_root": str(source_root), + "skills_root": str(explicit_skills_root), + "workspace_root": str(workspace), + "target_root": str(target), + "selected_skills": list(names), + "source_identity": source_identity, + "owner_validation": owner_validation, + "base_profile": { + key: value + for key, value in base_profile.items() + if key != "raw_b64" + }, + "source_metadata": { + key: value + for key, value in metadata.items() + if key not in {"graph", "capabilities_manifest", "skills_manifest"} + }, + "diagnostics": diagnostics, + } + raise PreflightError(diagnostics, payload) + return Plan( + source_root=source_root, + skills_root=explicit_skills_root, + workspace_root=workspace, + target_root=target, + source_identity=source_identity, + owner_validation=owner_validation, + base_profile=base_profile, + source_metadata=metadata, + selected_skills=names, + selected_roots=selected_roots, + source_roots=source_roots, + target_roots=target_roots, + source_files=selected_files, + target_files=target_files, + unselected_token=unselected_token, + source_token=source_token, + target_cas_token=target_cas_token, + previous_receipt=previous_receipt, + ) + + +def _plan_payload(plan: Plan) -> dict[str, Any]: + return { + "source_root": str(plan.source_root), + "skills_root": str(plan.skills_root), + "workspace_root": str(plan.workspace_root), + "target_root": str(plan.target_root), + "selected_skills": list(plan.selected_skills), + "selected_roots": [root.as_posix() for root in plan.selected_roots], + "selected_paths": sorted(plan.source_files), + "source_identity": plan.source_identity, + "owner_validation": plan.owner_validation, + "base_profile": { + key: value for key, value in plan.base_profile.items() if key != "raw_b64" + }, + "source_metadata": { + key: value + for key, value in plan.source_metadata.items() + if key not in {"graph", "capabilities_manifest", "skills_manifest"} + }, + "previous_receipt": { + key: value + for key, value in plan.previous_receipt.items() + if key != "raw_b64" + }, + "claim_limit": ( + "provenance and selected-byte parity only; no prompt selection, " + "invocation, routing quality, runtime health, or outcome claim" + ), + } + + +def _copy_entry(source: Path, target: Path) -> None: + if source.is_symlink(): + raise SkillProjectionError(f"cannot copy symlinked source entry: {source}") + if source.is_dir(): + shutil.copytree(source, target, copy_function=shutil.copy2) + elif source.is_file(): + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, target) + else: + raise SkillProjectionError(f"cannot copy non-regular source entry: {source}") + + +def _prepare_backup(plan: Plan, backup_root: Path) -> dict[str, Any]: + backup_root.mkdir(parents=True, exist_ok=False) + paths_root = backup_root / "paths" + paths_root.mkdir() + rows: list[dict[str, Any]] = [] + for index, relative in enumerate(plan.selected_roots): + source_path = plan.target_root / relative + stored = paths_root / str(index) + snapshot = next( + item for item in plan.target_roots if item.get("path") == relative.as_posix() + ) + if snapshot.get("state") == "absent": + rows.append({ + "index": index, + "path": relative.as_posix(), + "state": "absent", + "snapshot": snapshot, + }) + continue + _copy_entry(source_path, stored) + rows.append({ + "index": index, + "path": relative.as_posix(), + "state": str(snapshot.get("state")), + "snapshot": snapshot, + "stored": stored.relative_to(backup_root).as_posix(), + }) + previous = plan.previous_receipt + previous_state = { + "present": bool(previous.get("present")), + "sha256": previous.get("sha256"), + "bytes": previous.get("bytes"), + } + (backup_root / PREVIOUS_RECEIPT_STATE_NAME).write_text( + json.dumps(previous_state, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + ) + if previous.get("present"): + (backup_root / PREVIOUS_RECEIPT_NAME).write_bytes( + base64.b64decode(str(previous.get("raw_b64") or "")) + ) + manifest = { + "schema_version": "aoa_session_memory_skill_projection_backup_v1", + "operation_id": backup_root.name, + "created_at": _utc_now(), + "selected_roots": [root.as_posix() for root in plan.selected_roots], + "paths": rows, + "previous_receipt": previous_state, + } + manifest_path = backup_root / BACKUP_MANIFEST_NAME + _atomic_write( + manifest_path, + json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2).encode("utf-8") + b"\n", + prefix=".backup-manifest-", + ) + for path in sorted(backup_root.rglob("*"), key=lambda item: item.as_posix()): + if path.is_file(): + _fsync_file(path) + _fsync_dir(paths_root) + _fsync_dir(backup_root) + return manifest + + +def _stage(plan: Plan, stage_root: Path) -> None: + for relative in plan.selected_roots: + source = plan.source_root / relative + staged = stage_root / relative + _copy_entry(source, staged) + for path in sorted(stage_root.rglob("*"), key=lambda item: item.as_posix()): + if path.is_file(): + _fsync_file(path) + _fsync_dir(stage_root) + + +def _replace_staged_root(stage_root: Path, target_root: Path, relative: Path) -> None: + staged = stage_root / relative + target = target_root / relative + if target.is_symlink(): + raise SkillProjectionError(f"target selected path became a symlink: {relative.as_posix()}") + target.parent.mkdir(parents=True, exist_ok=True) + _remove_path(target) + os.replace(staged, target) + _fsync_dir(target.parent) + + +def _validate_backup_manifest( + plan: Plan, + backup_root: Path, + manifest: Mapping[str, Any], + *, + expected_before: Sequence[Mapping[str, Any]], + expected_previous_receipt: Mapping[str, Any], +) -> None: + """Validate a durable backup completely before any restore mutation.""" + if manifest.get("schema_version") != "aoa_session_memory_skill_projection_backup_v1": + raise SkillProjectionError("backup manifest schema is unsupported") + if str(manifest.get("operation_id") or "") != backup_root.name: + raise SkillProjectionError("backup manifest operation id does not match its path") + expected_roots = [root.as_posix() for root in plan.selected_roots] + if manifest.get("selected_roots") != expected_roots: + raise SkillProjectionError("backup manifest selected roots differ from receipt") + paths = manifest.get("paths") + if not isinstance(paths, list) or len(paths) != len(expected_roots): + raise SkillProjectionError("backup manifest selected path rows are incomplete") + rows_by_index: dict[int, Mapping[str, Any]] = {} + for item in paths: + if not isinstance(item, Mapping): + raise SkillProjectionError("backup manifest selected path row is invalid") + try: + index = int(item.get("index")) + except (TypeError, ValueError) as exc: + raise SkillProjectionError("backup manifest selected path index is invalid") from exc + if index in rows_by_index or index < 0 or index >= len(expected_roots): + raise SkillProjectionError("backup manifest selected path indexes are not exact") + if str(item.get("path") or "") != expected_roots[index]: + raise SkillProjectionError("backup manifest selected path order differs from receipt") + rows_by_index[index] = item + if set(rows_by_index) != set(range(len(expected_roots))): + raise SkillProjectionError("backup manifest selected path indexes are incomplete") + if len(expected_before) != len(expected_roots): + raise SkillProjectionError("expected selected before-state is incomplete") + for index, relative in enumerate(plan.selected_roots): + row = rows_by_index[index] + snapshot = row.get("snapshot") + if not isinstance(snapshot, Mapping) or not _root_snapshots_equal(snapshot, expected_before[index]): + raise SkillProjectionError(f"backup snapshot differs from receipt before-state: {relative.as_posix()}") + state = str(row.get("state") or "") + if state != str(snapshot.get("state") or ""): + raise SkillProjectionError(f"backup row state differs from snapshot: {relative.as_posix()}") + if state == "absent": + if row.get("stored") is not None: + raise SkillProjectionError(f"absent backup unexpectedly has stored path: {relative.as_posix()}") + continue + stored_text = str(row.get("stored") or "") + stored = _path_inside(backup_root, Path(stored_text), label="backup stored path") + if not stored.exists() or stored.is_symlink(): + raise SkillProjectionError(f"backup stored path is missing or symlinked: {relative.as_posix()}") + stored_snapshot = _snapshot_tree(stored, relative_root=relative) + if not _root_snapshots_equal(stored_snapshot, snapshot): + raise SkillProjectionError(f"backup stored bytes or modes differ: {relative.as_posix()}") + previous = manifest.get("previous_receipt") + if not isinstance(previous, Mapping): + raise SkillProjectionError("backup previous receipt state is missing") + for key in ("present", "sha256", "bytes"): + if previous.get(key) != expected_previous_receipt.get(key): + raise SkillProjectionError("backup previous receipt state differs from receipt") + if bool(previous.get("present")): + previous_path = backup_root / PREVIOUS_RECEIPT_NAME + if previous_path.is_symlink() or not previous_path.is_file(): + raise SkillProjectionError("backup previous component receipt is missing") + raw = previous_path.read_bytes() + if _sha256_bytes(raw) != previous.get("sha256") or len(raw) != previous.get("bytes"): + raise SkillProjectionError("backup previous component receipt bytes differ") + previous_state_path = backup_root / PREVIOUS_RECEIPT_STATE_NAME + if previous_state_path.is_symlink() or not previous_state_path.is_file(): + raise SkillProjectionError("backup previous receipt state file is missing") + stored_previous_state = _load_json( + previous_state_path, + label="backup previous receipt state", + ) + if stored_previous_state != dict(previous): + raise SkillProjectionError("backup previous receipt state file differs from manifest") + + +def _restore_backup( + plan: Plan, + backup_root: Path, + manifest: Mapping[str, Any], + *, + only_roots: Sequence[Path] | None = None, + restore_receipt: bool = True, +) -> None: + paths = manifest.get("paths") + if not isinstance(paths, list): + raise SkillProjectionError("backup manifest selected paths are missing") + allowed_roots = { + relative.as_posix() for relative in only_roots + } if only_roots is not None else None + for row in sorted( + (item for item in paths if isinstance(item, Mapping)), + key=lambda item: int(item.get("index") or 0), + ): + relative = _safe_relative(str(row.get("path") or ""), label="backup selected path") + if allowed_roots is not None and relative.as_posix() not in allowed_roots: + continue + state = str(row.get("state") or "") + snapshot = row.get("snapshot") + if isinstance(snapshot, Mapping) and state != str(snapshot.get("state") or ""): + raise SkillProjectionError(f"backup row state differs from snapshot: {relative.as_posix()}") + target = _path_inside(plan.target_root, relative, label="backup selected path") + _remove_path(target) + if state == "absent": + continue + stored_text = str(row.get("stored") or "") + stored = _path_inside(backup_root, Path(stored_text), label="backup stored path") + if not stored.exists() or stored.is_symlink(): + raise SkillProjectionError(f"backup stored path is missing or symlinked: {relative.as_posix()}") + _copy_entry(stored, target) + if not restore_receipt: + return + previous = manifest.get("previous_receipt") + if not isinstance(previous, Mapping): + raise SkillProjectionError("backup previous receipt state is missing") + receipt = _path_inside(plan.target_root, RECEIPT_RELATIVE, label="component receipt") + _remove_path(receipt) + if bool(previous.get("present")): + previous_path = backup_root / PREVIOUS_RECEIPT_NAME + if previous_path.is_symlink() or not previous_path.is_file(): + raise SkillProjectionError("backup previous component receipt is missing") + receipt.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(previous_path, receipt) + _fsync_dir(plan.target_root / "diagnostics") + + +def _selected_post_records(plan: Plan) -> tuple[dict[str, Any], ...]: + return _snapshot_roots(plan.target_root, plan.selected_roots) + + +def _receipt_payload( + plan: Plan, + *, + operation_id: str, + backup_root: Path, + before_roots: Sequence[Mapping[str, Any]], + after_roots: Sequence[Mapping[str, Any]], +) -> dict[str, Any]: + installed_at = _utc_now() + basis = { + "schema_version": SCHEMA_VERSION, + "operation_id": operation_id, + "source_commit": plan.source_identity.get("source_commit"), + "source_tree": plan.source_identity.get("source_tree"), + "source_script_sha256": plan.source_identity.get("source_script_sha256"), + "base_profile_sha256": plan.base_profile.get("sha256"), + "base_install_id": plan.base_profile.get("install_id"), + "selected_paths": sorted(plan.source_files), + "installed_at": installed_at, + } + component_install_id = "sha256:" + _sha256_json(basis) + after_files = _flatten_roots(after_roots) + return { + "schema_version": SCHEMA_VERSION, + "artifact_type": "runtime_skill_projection_install", + "component": "session-memory-skill-interface", + "component_install_id": component_install_id, + "operation_id": operation_id, + "installed_at": installed_at, + "workspace_root": str(plan.workspace_root), + "aoa_root": str(plan.target_root), + "source_root": str(plan.source_root), + "skills_root": str(plan.skills_root), + "source_worktree_clean": bool(plan.source_identity.get("source_worktree_clean")), + "source_identity": { + key: plan.source_identity.get(key) + for key in ( + "source_ref", + "source_root", + "source_commit", + "source_tree", + "source_script", + "source_script_sha256", + "source_worktree_clean", + ) + }, + "installer_script_sha256": "sha256:" + _sha256_file(Path(__file__).resolve()), + "shared_contract": plan.owner_validation.get("shared_contract"), + "owner_validation": { + "status": plan.owner_validation.get("status"), + "returncode": plan.owner_validation.get("returncode"), + }, + "base_profile": { + "path": plan.base_profile.get("path"), + "sha256": plan.base_profile.get("sha256"), + "bytes": plan.base_profile.get("bytes"), + "install_id": plan.base_profile.get("install_id"), + "source_ref": plan.base_profile.get("source_ref"), + "source_commit": plan.base_profile.get("source_commit"), + "source_tree": plan.base_profile.get("source_tree"), + "source_script_sha256": plan.base_profile.get("source_script_sha256"), + }, + "selected_skills": list(plan.selected_skills), + "selected_roots": [root.as_posix() for root in plan.selected_roots], + "selected_paths": sorted(plan.source_files), + "selected_before": list(before_roots), + "selected_after": list(after_roots), + "selected_after_files": after_files, + "packages": plan.source_metadata.get("packages", []), + "graph": { + "content_hash": plan.source_metadata.get("graph_source_content_hash"), + "json_sha256": plan.source_metadata.get("graph_json_sha256"), + "markdown_sha256": plan.source_metadata.get("graph_markdown_sha256"), + "router_sha256": plan.source_metadata.get("router_sha256"), + }, + "base_profile_sha256": plan.base_profile.get("sha256"), + "base_install_id": plan.base_profile.get("install_id"), + "previous_component_receipt_sha256": plan.previous_receipt.get("sha256"), + "previous_component_receipt": { + key: plan.previous_receipt.get(key) + for key in ("present", "sha256", "bytes") + }, + "backup_root": str(backup_root), + "unselected_target_token": plan.unselected_token, + "claim_limit": ( + "provenance and selected-byte parity only; no prompt selection, " + "invocation, routing quality, runtime health, or outcome claim" + ), + } + + +def _receipt_bytes(receipt: Mapping[str, Any]) -> bytes: + return json.dumps( + receipt, + ensure_ascii=False, + sort_keys=True, + indent=2, + ).encode("utf-8") + b"\n" + + +def _restore_after_failure( + plan: Plan, + backup_root: Path, + manifest: Mapping[str, Any], + *, + stage_root: Path | None, + replaced_roots: Sequence[Path], + restore_receipt: bool, +) -> tuple[bool, str | None]: + try: + _validate_backup_manifest( + plan, + backup_root, + manifest, + expected_before=plan.target_roots, + expected_previous_receipt=plan.previous_receipt, + ) + _restore_backup( + plan, + backup_root, + manifest, + only_roots=replaced_roots, + restore_receipt=restore_receipt, + ) + if stage_root is not None: + _remove_path(stage_root) + return True, None + except Exception as exc: # keep the durable backup when recovery itself fails + return False, str(exc) + + +def inspect_skill_projection( + *, + source_aoa_root: Path | str, + skills_root: Path | str, + workspace_root: Path | str, + aoa_root: Path | str, + selected_skills: Sequence[str] | None = None, +) -> dict[str, Any]: + """Return a read-only preflight payload.""" + try: + plan = _preflight( + source_aoa_root=source_aoa_root, + skills_root=skills_root, + workspace_root=workspace_root, + aoa_root=aoa_root, + selected_skills=selected_skills, + ) + except PreflightError as exc: + payload = dict(exc.payload) + payload.setdefault("schema_version", SCHEMA_VERSION) + payload["ok"] = False + payload["status"] = "preflight_failed" + payload["diagnostics"] = list(exc.diagnostics) + return payload + payload = _plan_payload(plan) + payload.update({"schema_version": SCHEMA_VERSION, "ok": True, "status": "ready"}) + return payload + + +def install_skill_projection( + *, + source_aoa_root: Path | str, + skills_root: Path | str, + workspace_root: Path | str, + aoa_root: Path | str, + selected_skills: Sequence[str] | None = None, + force: bool = False, + replace_hook: Callable[[str, int], None] | None = None, + before_commit_hook: Callable[[Plan], None] | None = None, +) -> dict[str, Any]: + """Apply one source-owned skill projection with exact rollback on error.""" + try: + plan = _preflight( + source_aoa_root=source_aoa_root, + skills_root=skills_root, + workspace_root=workspace_root, + aoa_root=aoa_root, + selected_skills=selected_skills, + ) + except PreflightError as exc: + payload = dict(exc.payload) + payload.update({ + "schema_version": SCHEMA_VERSION, + "ok": False, + "status": "preflight_failed", + "diagnostics": list(exc.diagnostics), + }) + return payload + changed = any( + not _root_snapshots_equal(source, target) + for source, target in zip(plan.source_roots, plan.target_roots) + ) + receipt_missing = not bool(plan.previous_receipt.get("present")) + receipt_currentness: list[str] = [] + if not receipt_missing: + receipt_path = _path_inside( + plan.target_root, + RECEIPT_RELATIVE, + label="component receipt", + ) + try: + existing_receipt = _load_json(receipt_path, label="component receipt") + if not isinstance(existing_receipt, Mapping): + receipt_currentness = ["component_receipt_not_object"] + else: + receipt_currentness = _receipt_currentness_diagnostics( + plan, + existing_receipt, + ) + except SkillProjectionError as exc: + receipt_currentness = [str(exc)] + if changed and not force: + payload = _plan_payload(plan) + payload.update({ + "schema_version": SCHEMA_VERSION, + "ok": False, + "status": "force_required", + "diagnostics": [ + "selected component differs from target; pass --force to authorize replacement" + ], + }) + return payload + if not changed and not receipt_missing and receipt_currentness and not force: + payload = _plan_payload(plan) + payload.update({ + "schema_version": SCHEMA_VERSION, + "ok": False, + "status": "receipt_stale", + "diagnostics": receipt_currentness, + }) + return payload + if not changed and not receipt_missing and not receipt_currentness: + payload = _plan_payload(plan) + payload.update({ + "schema_version": SCHEMA_VERSION, + "ok": True, + "status": "already_current", + "diagnostics": [], + }) + return payload + + operation_id = f"{int(time.time())}-{uuid.uuid4().hex}" + backup_root = plan.target_root / BACKUP_PARENT_RELATIVE / operation_id + stage_root: Path | None = None + backup_manifest: dict[str, Any] | None = None + mutation_started = False + replaced_roots: list[Path] = [] + receipt_mutation_started = False + try: + backup_manifest = _prepare_backup(plan, backup_root) + stage_root = Path(tempfile.mkdtemp(prefix=STAGE_PREFIX, dir=str(plan.target_root))) + _stage(plan, stage_root) + if before_commit_hook is not None: + before_commit_hook(plan) + # Source and target are both CAS participants. A source edit after + # validation is as unsafe as a target edit, even before any rename. + current_source_identity = runtime_install_source_provenance(plan.source_root) + current_source_files = { + relative: _record(plan.source_root / relative) + for relative in sorted(plan.source_files) + } + if _source_identity_token(current_source_identity, current_source_files) != plan.source_token: + raise SkillProjectionError("compare_and_swap_conflict: source changed after preflight") + if _target_cas_token(plan) != plan.target_cas_token: + raise SkillProjectionError("compare_and_swap_conflict: target changed after preflight") + for index, relative in enumerate(plan.selected_roots): + if replace_hook is not None: + replace_hook(relative.as_posix(), index) + mutation_started = True + # Mark the root before entering the remove/rename pair. If the + # second rename fails, the first one may already have removed the + # old target and this root must still be restored. + replaced_roots.append(relative) + _replace_staged_root(stage_root, plan.target_root, relative) + after_roots = _selected_post_records(plan) + if any( + not _root_snapshots_equal(source, after) + for source, after in zip(plan.source_roots, after_roots) + ): + raise SkillProjectionError("post-apply selected bytes or modes differ from source") + profile_path = plan.target_root / _session_memory.INSTALL_PROFILE_PATH + if _sha256_file(profile_path) != plan.base_profile.get("sha256"): + raise SkillProjectionError("base install profile changed during skill overlay") + parity_diagnostics, _records, parity_token = _unselected_parity( + plan.source_root, + plan.target_root, + graph=plan.source_metadata["graph"], + projection_paths=set(plan.source_metadata.get("graph_projection_paths", [])), + selected_skills=set(plan.selected_skills), + selected_files=set(plan.source_files), + ) + if parity_diagnostics or parity_token != plan.unselected_token: + raise SkillProjectionError( + "post-apply unselected source parity changed: " + + ", ".join(parity_diagnostics[:8]) + ) + receipt = _receipt_payload( + plan, + operation_id=operation_id, + backup_root=backup_root, + before_roots=plan.target_roots, + after_roots=after_roots, + ) + receipt_mutation_started = True + mutation_started = True + _atomic_write( + plan.target_root / RECEIPT_RELATIVE, + _receipt_bytes(receipt), + prefix=".skill-projection-receipt-", + ) + if stage_root is not None: + _remove_path(stage_root) + return { + "schema_version": SCHEMA_VERSION, + "ok": True, + "status": "installed", + "receipt": receipt, + "backup_root": str(backup_root), + "plan": _plan_payload(plan), + } + except Exception as exc: + if backup_manifest is not None and mutation_started: + restored, restore_error = _restore_after_failure( + plan, + backup_root, + backup_manifest, + stage_root=stage_root, + replaced_roots=replaced_roots, + restore_receipt=receipt_mutation_started, + ) + if restored: + # A failed operation has no live component receipt; leave its + # durable backup only when the caller needs post-mortem repair. + _remove_path(backup_root) + return { + "schema_version": SCHEMA_VERSION, + "ok": False, + "status": "install_failed_rolled_back", + "diagnostics": [str(exc)], + } + return { + "schema_version": SCHEMA_VERSION, + "ok": False, + "status": "install_failed_rollback_incomplete", + "diagnostics": [str(exc), f"rollback failed: {restore_error}"], + "backup_root": str(backup_root), + } + # A CAS conflict is detected before the first selected-root rename. + # Never restore the preflight snapshot over a concurrent edit. Only + # our temporary stage and durable backup are owned at this point. + if stage_root is not None: + _remove_path(stage_root) + if backup_root.exists(): + _remove_path(backup_root) + conflict_status = ( + "compare_and_swap_conflict" + if "compare_and_swap_conflict" in str(exc) + else "install_failed_before_mutation" + ) + return { + "schema_version": SCHEMA_VERSION, + "ok": False, + "status": conflict_status, + "diagnostics": [str(exc)], + } + + +def _load_receipt(target_root: Path) -> tuple[dict[str, Any], bytes]: + path = _path_inside(target_root, RECEIPT_RELATIVE, label="component receipt") + if path.is_symlink() or not path.is_file(): + raise SkillProjectionError("component receipt is absent or not a regular file") + raw = path.read_bytes() + payload = _load_json(path, label="component receipt") + if not isinstance(payload, Mapping) or payload.get("schema_version") != SCHEMA_VERSION: + raise SkillProjectionError("component receipt schema is unsupported") + return dict(payload), raw + + +def _receipt_after_matches(plan: Plan, receipt: Mapping[str, Any]) -> bool: + selected_roots = receipt.get("selected_roots") + if selected_roots != [root.as_posix() for root in plan.selected_roots]: + return False + expected = receipt.get("selected_after") + if not isinstance(expected, list): + return False + current = _selected_post_records(plan) + if len(current) != len(expected): + return False + if any(not isinstance(expected_item, Mapping) for expected_item in expected): + return False + return all( + _root_snapshots_equal(actual, expected_item) + for actual, expected_item in zip(current, expected) + ) + + +def _receipt_currentness_diagnostics( + plan: Plan, + receipt: Mapping[str, Any], +) -> list[str]: + """Return concrete reasons an existing receipt is not current for ``plan``. + + A matching selected tree is insufficient for ``already_current``. The + receipt is also a provenance anchor for the source, shared contract, + installer implementation, graph/package identities, and the unchanged + base runtime profile. Keep this check read-only so a stale or tampered + receipt can never be silently accepted. + """ + diagnostics: list[str] = [] + if receipt.get("schema_version") != SCHEMA_VERSION: + diagnostics.append("component_receipt_schema_mismatch") + if receipt.get("artifact_type") != "runtime_skill_projection_install": + diagnostics.append("component_receipt_artifact_type_mismatch") + if receipt.get("component") != "session-memory-skill-interface": + diagnostics.append("component_receipt_component_mismatch") + if str(receipt.get("workspace_root") or "") != str(plan.workspace_root): + diagnostics.append("component_receipt_workspace_root_mismatch") + if str(receipt.get("aoa_root") or "") != str(plan.target_root): + diagnostics.append("component_receipt_target_root_mismatch") + if str(receipt.get("source_root") or "") != str(plan.source_root): + diagnostics.append("component_receipt_source_root_mismatch") + if str(receipt.get("skills_root") or "") != str(plan.skills_root): + diagnostics.append("component_receipt_skills_root_mismatch") + + identity = receipt.get("source_identity") + if not isinstance(identity, Mapping): + diagnostics.append("component_receipt_source_identity_missing") + else: + for key in ( + "source_ref", + "source_root", + "source_commit", + "source_tree", + "source_script", + "source_script_sha256", + "source_worktree_clean", + ): + if identity.get(key) != plan.source_identity.get(key): + diagnostics.append(f"component_receipt_source_{key}_mismatch") + if receipt.get("source_worktree_clean") is not True: + diagnostics.append("component_receipt_source_not_clean") + + expected_installer_digest = "sha256:" + _sha256_file(Path(__file__).resolve()) + if receipt.get("installer_script_sha256") != expected_installer_digest: + diagnostics.append("component_receipt_installer_script_mismatch") + + base = receipt.get("base_profile") + if not isinstance(base, Mapping): + diagnostics.append("component_receipt_base_profile_missing") + else: + for key in ( + "path", + "sha256", + "bytes", + "install_id", + "source_ref", + "source_commit", + "source_tree", + "source_script_sha256", + ): + if base.get(key) != plan.base_profile.get(key): + diagnostics.append(f"component_receipt_base_profile_{key}_mismatch") + if receipt.get("base_profile_sha256") != plan.base_profile.get("sha256"): + diagnostics.append("component_receipt_base_profile_sha256_mismatch") + if receipt.get("base_install_id") != plan.base_profile.get("install_id"): + diagnostics.append("component_receipt_base_install_id_mismatch") + + previous_state = receipt.get("previous_component_receipt") + if not isinstance(previous_state, Mapping): + diagnostics.append("component_receipt_previous_state_missing") + else: + if receipt.get("previous_component_receipt_sha256") != previous_state.get("sha256"): + diagnostics.append("component_receipt_previous_state_anchor_mismatch") + if previous_state.get("present") not in {True, False}: + diagnostics.append("component_receipt_previous_state_present_invalid") + if not isinstance(previous_state.get("bytes"), int) or int(previous_state.get("bytes")) < 0: + diagnostics.append("component_receipt_previous_state_bytes_invalid") + + if receipt.get("selected_skills") != list(plan.selected_skills): + diagnostics.append("component_receipt_selected_skills_mismatch") + if receipt.get("selected_roots") != [root.as_posix() for root in plan.selected_roots]: + diagnostics.append("component_receipt_selected_roots_mismatch") + if receipt.get("selected_paths") != sorted(plan.source_files): + diagnostics.append("component_receipt_selected_paths_mismatch") + if not _receipt_after_matches(plan, receipt): + diagnostics.append("component_receipt_selected_after_mismatch") + after_files = receipt.get("selected_after_files") + if not isinstance(after_files, Mapping): + diagnostics.append("component_receipt_selected_after_files_missing") + elif dict(after_files) != _flatten_roots(_selected_post_records(plan)): + diagnostics.append("component_receipt_selected_after_files_mismatch") + + packages = receipt.get("packages") + expected_packages = plan.source_metadata.get("packages", []) + if packages != expected_packages: + diagnostics.append("component_receipt_package_identity_mismatch") + graph = receipt.get("graph") + expected_graph = { + "content_hash": plan.source_metadata.get("graph_source_content_hash"), + "json_sha256": plan.source_metadata.get("graph_json_sha256"), + "markdown_sha256": plan.source_metadata.get("graph_markdown_sha256"), + "router_sha256": plan.source_metadata.get("router_sha256"), + } + if graph != expected_graph: + diagnostics.append("component_receipt_graph_identity_mismatch") + if receipt.get("shared_contract") != plan.owner_validation.get("shared_contract"): + diagnostics.append("component_receipt_shared_contract_mismatch") + if receipt.get("unselected_target_token") != plan.unselected_token: + diagnostics.append("component_receipt_unselected_target_mismatch") + + backup_text = str(receipt.get("backup_root") or "") + if not backup_text: + diagnostics.append("component_receipt_backup_root_missing") + else: + backup = _resolve(Path(backup_text)) + expected_parent = _resolve(plan.target_root / BACKUP_PARENT_RELATIVE) + try: + backup.relative_to(expected_parent) + except ValueError: + diagnostics.append("component_receipt_backup_root_escapes_target") + else: + if backup.is_symlink() or not backup.is_dir(): + diagnostics.append("component_receipt_backup_missing") + elif not (backup / BACKUP_MANIFEST_NAME).is_file(): + diagnostics.append("component_receipt_backup_manifest_missing") + return list(dict.fromkeys(diagnostics)) + + +def _rollback_expected_roots(selected_skills: Sequence[str]) -> tuple[Path, ...]: + """Derive the closed top-level root allowlist from a receipt. + + Rollback intentionally does not re-admit the source graph. The receipt + records the selected skill names, so the safe restore surface is the + fixed interface closure plus ``skills/`` package directories. + """ + names = tuple(str(item) for item in selected_skills) + if not names or tuple(dict.fromkeys(names)) != names: + raise SkillProjectionError("rollback receipt selected skills are not unique") + roots: list[Path] = list(FIXED_SOURCE_RELATIVE) + for name in names: + roots.append(Path("skills") / _safe_skill_name(name)) + if "aoa-session-memory-global-route" not in names: + roots.append(GLOBAL_ROUTER_RELATIVE) + return tuple(dict.fromkeys(roots)) + + +def _validate_receipt_snapshot_rows( + snapshots: Sequence[Any], + expected_roots: Sequence[Path], + *, + label: str, +) -> list[str]: + """Validate receipt root/file shape and keep paths inside the allowlist.""" + diagnostics: list[str] = [] + if len(snapshots) != len(expected_roots): + return [f"{label}_count_mismatch"] + seen_paths: set[str] = set() + for index, (item, root) in enumerate(zip(snapshots, expected_roots)): + if not isinstance(item, Mapping): + diagnostics.append(f"{label}_{index}_not_object") + continue + if item.get("path") != root.as_posix(): + diagnostics.append(f"{label}_{index}_path_mismatch") + state = str(item.get("state") or "") + if state not in {"file", "directory", "absent"}: + diagnostics.append(f"{label}_{index}_state_invalid") + files = item.get("files") + if not isinstance(files, list): + diagnostics.append(f"{label}_{index}_files_invalid") + continue + if state == "file" and len(files) != 1: + diagnostics.append(f"{label}_{index}_file_row_count_invalid") + if state == "absent" and files: + diagnostics.append(f"{label}_{index}_absent_has_files") + for row in files: + if not isinstance(row, Mapping): + diagnostics.append(f"{label}_{index}_file_row_invalid") + continue + path_text = str(row.get("path") or "") + try: + relative = _safe_relative(path_text, label=f"{label} file") + except SkillProjectionError: + diagnostics.append(f"{label}_{index}_file_path_invalid") + continue + relative_text = relative.as_posix() + if relative_text in seen_paths: + diagnostics.append(f"{label}_duplicate_file:{relative_text}") + seen_paths.add(relative_text) + if not ( + relative_text == root.as_posix() + or relative_text.startswith(root.as_posix() + "/") + ): + diagnostics.append(f"{label}_{index}_file_escapes_root:{relative_text}") + if row.get("state") != "file": + diagnostics.append(f"{label}_{index}_file_state_invalid") + digest = str(row.get("sha256") or "") + if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest.lower()): + diagnostics.append(f"{label}_{index}_file_digest_invalid") + if not isinstance(row.get("bytes"), int) or int(row.get("bytes")) < 0: + diagnostics.append(f"{label}_{index}_file_bytes_invalid") + if not isinstance(row.get("mode"), int) or int(row.get("mode")) < 0: + diagnostics.append(f"{label}_{index}_file_mode_invalid") + return diagnostics + + +def _rollback_receipt_diagnostics( + plan: Plan, + receipt: Mapping[str, Any], +) -> list[str]: + """Validate receipt self-containment without source or owner re-admission.""" + diagnostics: list[str] = [] + if receipt.get("schema_version") != SCHEMA_VERSION: + diagnostics.append("component_receipt_schema_mismatch") + if receipt.get("artifact_type") != "runtime_skill_projection_install": + diagnostics.append("component_receipt_artifact_type_mismatch") + if receipt.get("component") != "session-memory-skill-interface": + diagnostics.append("component_receipt_component_mismatch") + if receipt.get("workspace_root") != str(plan.workspace_root): + diagnostics.append("component_receipt_workspace_root_mismatch") + if receipt.get("aoa_root") != str(plan.target_root): + diagnostics.append("component_receipt_target_root_mismatch") + selected_skills = receipt.get("selected_skills") + if ( + not isinstance(selected_skills, list) + or not selected_skills + or any(not isinstance(item, str) for item in selected_skills) + ): + diagnostics.append("component_receipt_selected_skills_missing") + selected_skills = [] + try: + expected_roots = _rollback_expected_roots(selected_skills) + except SkillProjectionError as exc: + diagnostics.append(str(exc)) + expected_roots = tuple() + selected_roots = receipt.get("selected_roots") + if not isinstance(selected_roots, list): + diagnostics.append("component_receipt_selected_roots_missing") + elif selected_roots != [root.as_posix() for root in expected_roots]: + diagnostics.append("component_receipt_selected_roots_outside_closed_allowlist") + before = receipt.get("selected_before") + after = receipt.get("selected_after") + if not isinstance(before, list) or not isinstance(after, list): + diagnostics.append("component_receipt_selected_snapshots_missing") + before = [] + after = [] + else: + diagnostics.extend( + _validate_receipt_snapshot_rows( + before, + expected_roots, + label="component_receipt_before", + ) + ) + diagnostics.extend( + _validate_receipt_snapshot_rows( + after, + expected_roots, + label="component_receipt_after", + ) + ) + if expected_roots and not _receipt_after_matches(plan, receipt): + diagnostics.append("component_receipt_selected_after_mismatch") + after_files = receipt.get("selected_after_files") + flattened_after = _flatten_roots(after) + if not isinstance(after_files, Mapping): + diagnostics.append("component_receipt_selected_after_files_missing") + elif dict(after_files) != flattened_after: + diagnostics.append("component_receipt_selected_after_files_mismatch") + selected_paths = receipt.get("selected_paths") + if not isinstance(selected_paths, list) or selected_paths != sorted(flattened_after): + diagnostics.append("component_receipt_selected_paths_mismatch") + previous_state = receipt.get("previous_component_receipt") + if not isinstance(previous_state, Mapping): + diagnostics.append("component_receipt_previous_state_missing") + else: + if receipt.get("previous_component_receipt_sha256") != previous_state.get("sha256"): + diagnostics.append("component_receipt_previous_state_anchor_mismatch") + if previous_state.get("present") not in {True, False}: + diagnostics.append("component_receipt_previous_state_present_invalid") + if type(previous_state.get("bytes")) is not int or int(previous_state.get("bytes")) < 0: + diagnostics.append("component_receipt_previous_state_bytes_invalid") + if not isinstance(receipt.get("unselected_target_token"), str) or not receipt.get( + "unselected_target_token" + ): + diagnostics.append("component_receipt_unselected_target_anchor_missing") + base_profile = receipt.get("base_profile") + if not isinstance(base_profile, Mapping): + diagnostics.append("component_receipt_base_profile_missing") + else: + if base_profile.get("path") != _session_memory.INSTALL_PROFILE_PATH.as_posix(): + diagnostics.append("component_receipt_base_profile_path_mismatch") + if type(base_profile.get("bytes")) is not int or int(base_profile.get("bytes")) < 0: + diagnostics.append("component_receipt_base_profile_bytes_invalid") + if base_profile.get("sha256") != receipt.get("base_profile_sha256"): + diagnostics.append("component_receipt_base_profile_hash_mismatch") + if base_profile.get("install_id") != receipt.get("base_install_id"): + diagnostics.append("component_receipt_base_profile_install_id_mismatch") + return list(dict.fromkeys(diagnostics)) + + +def rollback_skill_projection( + *, + workspace_root: Path | str, + aoa_root: Path | str, + source_aoa_root: Path | str | None = None, + skills_root: Path | str | None = None, +) -> dict[str, Any]: + """Restore the exact prior selected component from its closed receipt. + + Rollback is deliberately independent of the candidate source and the + external owner checkout. Those trees may be dirty, moved, or unavailable + precisely when the last-good runtime overlay needs to be recovered. If + callers provide either root, it is treated as an optional identity check + against the receipt; it is never revalidated or used as a copy source. + """ + try: + workspace = _root_ready(Path(workspace_root), label="workspace root") + target = _root_ready(Path(aoa_root), label="target .aoa root") + receipt, receipt_raw = _load_receipt(target) + if str(receipt.get("workspace_root")) != str(workspace): + raise SkillProjectionError("rollback workspace root does not match receipt") + if str(receipt.get("aoa_root")) != str(target): + raise SkillProjectionError("rollback target root does not match receipt") + if source_aoa_root is not None: + source = _root_ready(Path(source_aoa_root), label="source .aoa root") + if str(receipt.get("source_root")) != str(source): + raise SkillProjectionError("rollback source root does not match receipt") + if skills_root is not None: + skills = _root_ready(Path(skills_root), label="aoa-skills contract root") + if str(receipt.get("skills_root")) != str(skills): + raise SkillProjectionError("rollback skills root does not match receipt") + + selected_skills = receipt.get("selected_skills") + if ( + not isinstance(selected_skills, list) + or not selected_skills + or any(not isinstance(item, str) for item in selected_skills) + ): + raise SkillProjectionError("rollback receipt selected skills are missing") + + selected_roots = _rollback_expected_roots(selected_skills) + plan = Plan( + source_root=target, + skills_root=target, + workspace_root=workspace, + target_root=target, + source_identity={}, + owner_validation={}, + base_profile={}, + source_metadata={"selected_files": {}}, + selected_skills=tuple(selected_skills), + selected_roots=selected_roots, + source_roots=tuple(), + target_roots=tuple(), + source_files={}, + target_files={}, + unselected_token=str(receipt.get("unselected_target_token") or ""), + source_token="", + target_cas_token="", + previous_receipt={}, + ) + receipt_diagnostics = _rollback_receipt_diagnostics(plan, receipt) + if receipt_diagnostics: + raise SkillProjectionError( + "rollback receipt is stale or tampered: " + + ", ".join(receipt_diagnostics) + ) + selected_roots_text = receipt.get("selected_roots") + if not isinstance(selected_roots_text, list) or not selected_roots_text: + raise SkillProjectionError("rollback receipt selected roots are missing") + before_roots = receipt.get("selected_before") + if not isinstance(before_roots, list) or len(before_roots) != len(plan.selected_roots): + raise SkillProjectionError("rollback receipt selected before-state is incomplete") + if any(not isinstance(item, Mapping) for item in before_roots): + raise SkillProjectionError("rollback receipt selected before-state is invalid") + previous_state = receipt.get("previous_component_receipt") + if not isinstance(previous_state, Mapping): + raise SkillProjectionError("rollback receipt previous component receipt state is missing") + if receipt.get("previous_component_receipt_sha256") != previous_state.get("sha256"): + raise SkillProjectionError("rollback receipt previous component receipt anchor mismatch") + + backup_text = str(receipt.get("backup_root") or "") + backup = _resolve(Path(backup_text)) + backup_parent = _resolve(target / BACKUP_PARENT_RELATIVE) + try: + backup.relative_to(backup_parent) + except ValueError as exc: + raise SkillProjectionError("rollback backup root escapes diagnostics backup area") from exc + if backup.is_symlink() or not backup.is_dir(): + raise SkillProjectionError("rollback durable backup is absent or symlinked") + manifest_path = backup / BACKUP_MANIFEST_NAME + manifest = _load_json(manifest_path, label="rollback backup manifest") + if not isinstance(manifest, Mapping): + raise SkillProjectionError("rollback backup manifest is not an object") + # The profile is an independent kernel owner anchor. Read it directly + # so rollback remains available when the source/validator checkout has + # disappeared, while still rejecting a target that changed underneath + # the receipt. + profile_path = _path_inside( + target, + _session_memory.INSTALL_PROFILE_PATH, + label="rollback base install profile", + ) + profile_raw = profile_path.read_bytes() + try: + profile_payload = json.loads(profile_raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise SkillProjectionError("rollback base install profile is invalid") from exc + if not isinstance(profile_payload, Mapping): + raise SkillProjectionError("rollback base install profile is not an object") + if profile_payload.get("workspace_root") != str(workspace): + raise SkillProjectionError("rollback base install profile workspace anchor changed") + if profile_payload.get("aoa_root") != str(target): + raise SkillProjectionError("rollback base install profile target anchor changed") + base_profile = receipt.get("base_profile") + if not isinstance(base_profile, Mapping): + raise SkillProjectionError("rollback receipt base profile anchor is missing") + profile_hash = _sha256_bytes(profile_raw) + if profile_hash != receipt.get("base_profile_sha256"): + raise SkillProjectionError("rollback base install profile anchor changed") + if profile_hash != base_profile.get("sha256"): + raise SkillProjectionError("rollback receipt base profile hash is inconsistent") + if len(profile_raw) != base_profile.get("bytes"): + raise SkillProjectionError("rollback base install profile byte anchor is inconsistent") + if profile_payload.get("install_id") != receipt.get("base_install_id"): + raise SkillProjectionError("rollback base install id anchor changed") + if profile_payload.get("install_id") != base_profile.get("install_id"): + raise SkillProjectionError("rollback receipt base install id is inconsistent") + if str(receipt.get("operation_id") or "") != backup.name: + raise SkillProjectionError("rollback receipt operation id does not match backup") + _validate_backup_manifest( + plan, + backup, + manifest, + expected_before=before_roots, + expected_previous_receipt=previous_state, + ) + except SkillProjectionError as exc: + return { + "schema_version": SCHEMA_VERSION, + "ok": False, + "status": "rollback_precondition_failed", + "diagnostics": [str(exc)], + } + + try: + _restore_backup(plan, backup, manifest) + restored_roots = _snapshot_roots(target, plan.selected_roots) + if any( + not _root_snapshots_equal(actual, expected) + for actual, expected in zip(restored_roots, before_roots) + ): + raise SkillProjectionError("rollback restored selected state differs from receipt before-state") + if _sha256_file(profile_path) != receipt.get("base_profile_sha256"): + raise SkillProjectionError("rollback changed base install profile") + restored_previous = _previous_receipt(target) + for key in ("present", "sha256", "bytes"): + if restored_previous.get(key) != previous_state.get(key): + raise SkillProjectionError("rollback restored previous receipt differs from backup state") + outcome = { + "schema_version": "aoa_session_memory_skill_projection_rollback_v1", + "artifact_type": "runtime_skill_projection_rollback", + "status": "rolled_back", + "ok": True, + "rolled_back_at": _utc_now(), + "workspace_root": str(workspace), + "aoa_root": str(target), + "component_receipt_sha256": _sha256_bytes(receipt_raw), + "restored_previous_receipt_sha256": ( + previous_state.get("sha256") + ), + "backup_root": str(backup), + "claim_limit": "selected component rollback outcome only", + } + _atomic_write( + target / ROLLBACK_OUTCOME_RELATIVE, + json.dumps(outcome, ensure_ascii=False, sort_keys=True, indent=2).encode("utf-8") + b"\n", + prefix=".skill-projection-rollback-", + ) + return outcome + except Exception as exc: + return { + "schema_version": "aoa_session_memory_skill_projection_rollback_v1", + "ok": False, + "status": "rollback_failed_after_precondition", + "diagnostics": [str(exc)], + "backup_root": str(backup), + } + + +def _add_common_roots(parser: argparse.ArgumentParser, *, require_source: bool) -> None: + if require_source: + parser.add_argument("--source-aoa-root", required=True) + parser.add_argument("--skills-root", required=True) + else: + parser.add_argument("--source-aoa-root") + parser.add_argument("--skills-root") + parser.add_argument("--workspace-root", required=True) + parser.add_argument("--aoa-root", required=True) + parser.add_argument( + "--skill", + action="append", + dest="skills", + metavar="NAME", + help=( + "Graph-declared skill package to include; repeat for multiple " + "packages. Defaults to the two advertised session-memory routers." + ), + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Install a bounded source-owned session-memory skill interface overlay." + ) + sub = parser.add_subparsers(dest="command", required=True) + check = sub.add_parser("check", help="Run the read-only skills overlay preflight.") + _add_common_roots(check, require_source=True) + check.set_defaults(_handler="check") + install = sub.add_parser( + "install", + aliases=["execute"], + help="Apply the selected overlay after explicit --force authorization.", + ) + _add_common_roots(install, require_source=True) + install.add_argument( + "--force", + action="store_true", + help="Authorize replacing selected component paths after preflight.", + ) + install.set_defaults(_handler="install") + rollback = sub.add_parser( + "rollback", + help="Restore the prior selected component from its durable backup.", + ) + rollback.add_argument("--workspace-root", required=True) + rollback.add_argument("--aoa-root", required=True) + rollback.add_argument( + "--source-aoa-root", + help="Optional source root identity check; rollback does not read or revalidate it.", + ) + rollback.add_argument( + "--skills-root", + help="Optional aoa-skills root identity check; rollback does not read or revalidate it.", + ) + rollback.set_defaults(_handler="rollback") + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + if args._handler == "check": + payload = inspect_skill_projection( + source_aoa_root=args.source_aoa_root, + skills_root=args.skills_root, + workspace_root=args.workspace_root, + aoa_root=args.aoa_root, + selected_skills=args.skills, + ) + elif args._handler == "install": + payload = install_skill_projection( + source_aoa_root=args.source_aoa_root, + skills_root=args.skills_root, + workspace_root=args.workspace_root, + aoa_root=args.aoa_root, + selected_skills=args.skills, + force=bool(args.force), + ) + else: + payload = rollback_skill_projection( + workspace_root=args.workspace_root, + aoa_root=args.aoa_root, + source_aoa_root=args.source_aoa_root, + skills_root=args.skills_root, + ) + print(json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2)) + return 0 if payload.get("ok") else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/AGENTS.md b/skills/AGENTS.md index 3f03c1b8..0a792dd0 100644 --- a/skills/AGENTS.md +++ b/skills/AGENTS.md @@ -2,7 +2,9 @@ ## Purpose -This directory holds Codex skills for deliberate session-memory workflows. +This directory holds agent skills for deliberate session-memory workflows. +The common routes use portable operations and evidence contracts. Codex hook, +transcript, and compaction procedures remain concrete adapter routes. Skills are route surfaces for agents. They should make recurring work repeatable without turning hooks into heavy interpretation. diff --git a/skills/aoa-session-memory-evidence-route/SKILL.md b/skills/aoa-session-memory-evidence-route/SKILL.md index edd9fba9..475ab2e8 100644 --- a/skills/aoa-session-memory-evidence-route/SKILL.md +++ b/skills/aoa-session-memory-evidence-route/SKILL.md @@ -19,6 +19,9 @@ The route finds and qualifies evidence. It never upgrades mention, prompt visibility, selection, loading, or a `SKILL.md` read into invocation or effectiveness. +Bind bounded queries through the available MCP or owner CLI. Archive reads +preserve the same evidence contract across models and do not verify live hooks. + ## Procedure 1. Resolve the logical `` and the entity kind. Start with the diff --git a/skills/aoa-session-memory-global-route/SKILL.md b/skills/aoa-session-memory-global-route/SKILL.md index cf54c5c4..27cd247b 100644 --- a/skills/aoa-session-memory-global-route/SKILL.md +++ b/skills/aoa-session-memory-global-route/SKILL.md @@ -1,6 +1,6 @@ --- name: aoa-session-memory-global-route -description: Use in any Codex session when the user mentions `.aoa`, session memory, Codex transcripts, compaction, prior session rehydration, hook failures, or AoA session-memory validation. +description: "Use when an agent needs to route `.aoa` session-memory work: evidence retrieval, archive lookup, rehydration, projection, validation, or an adapter-specific transcript, hook, or compaction operation." license: Apache-2.0 metadata: aoa_scope: session-memory @@ -22,16 +22,29 @@ Resolve three logical roots before acting: Concrete paths are runtime bindings, not portable skill identity. +## Required Capabilities + +Selection needs readable owner guidance and the capability router. Execution +needs the selected operation's actual inputs, permissions, and an available +read-only MCP or owner CLI binding. A filesystem read, a query response, and +an adapter lifecycle event are different capabilities; availability of one +does not imply the others. + +Use the same evidence and scope contract for every consumer. Model and +reasoning settings are execution configuration, not role identity, authority, +or a different acceptance rule. The home-port's consumer exposure declares +eligibility only; it does not install or activate a runtime. + ## Trigger Boundary -Use this skill in any Codex session when the task touches: +Use this skill when the task touches: - `.aoa` session memory -- Codex raw transcript JSONL -- context compaction or compaction intervals +- preserved session evidence or raw transcripts +- context continuity or archived compaction intervals - prior-session resume, rehydration, or session archive lookup -- AoA hooks for `SessionStart`, `UserPromptSubmit`, `PreCompact`, - `PostCompact`, or `Stop` +- capture hooks, including the Codex adapter's `SessionStart`, + `UserPromptSubmit`, `PreCompact`, `PostCompact`, or `Stop` - `raw_unavailable` incidents - `stress-pass`, `audit`, `doctor`, `codex-hooks-status`, or `codex-compact-probe` @@ -65,7 +78,11 @@ Use this skill in any Codex session when the task touches: [references/capability-router.md](references/capability-router.md), verify its source hash against the generated graph when composition matters, and choose the smallest applicable bundle. -4. Use `/scripts/aoa_session_memory.py` for commands. +4. Bind the selected operation to an actually available read-only MCP tool or + `/scripts/aoa_session_memory.py` CLI command. For Codex hooks, + transcript import, app-server or live compaction, select `adapters.codex` + and its exact requirements. Missing adapter capabilities block that + operation; they do not block an independent authorized archive read. 5. Keep historical raw/session material intact unless the user explicitly asks for a repair. 6. If the task changes portable behavior, export to diff --git a/skills/aoa-session-memory-global-route/references/capability-router.md b/skills/aoa-session-memory-global-route/references/capability-router.md index 0ec46ba2..b65456c6 100644 --- a/skills/aoa-session-memory-global-route/references/capability-router.md +++ b/skills/aoa-session-memory-global-route/references/capability-router.md @@ -2,13 +2,13 @@ Generated from owner capability contracts. This card is a retrieval read model, not procedure or proof authority. -Source graph hash: `f6d51e2a0f348f2444f337fa652440d931296924119cd3b503d3d25da1c3a74a` +Source graph hash: `8e0659361a4ff83f8b7bbcaea696e4dadfb41b3871bdec37b81a0825fd6a9fd7` Federation: `aoa-session-memory` specializes `aoa-skills:sessions`. | skill | visibility | use when | do not use when | version | fingerprint | |---|---|---|---|---|---| -| `aoa-session-memory-evidence-route` | advertised | The answer depends on what happened in prior .aoa sessions and must carry resolvable raw or segment refs. | Current repository or runtime state is the stronger source and no historical behavior question remains. | 1.0.0 | `383667122ea265f7` | -| `aoa-session-memory-global-route` | advertised | The request names session memory; .aoa; transcripts; compaction; rehydration; hooks; or session-memory validation. | A narrower non-session-memory owner is already known and no session evidence or session-memory operation is required. | 1.0.0 | `f459356ad356a679` | +| `aoa-session-memory-evidence-route` | advertised | The answer depends on what happened in prior .aoa sessions and must carry resolvable raw or segment refs. | Current repository or runtime state is the stronger source and no historical behavior question remains. | 1.0.1 | `2adca52eea64742a` | +| `aoa-session-memory-global-route` | advertised | The request names session memory; .aoa; transcripts; compaction; rehydration; hooks; or session-memory validation. | A narrower non-session-memory owner is already known and no session evidence or session-memory operation is required. | 1.0.1 | `d0e36e745baafe44` | | `aoa-codex-compact-probe` | deferred | The live Codex pre/post-compaction hook path requires an explicit behavioral probe. | Static hook registry inspection is sufficient and no behavioral probe is requested.; The runtime is not Codex. | 1.0.0 | `ef1990131b253144` | | `aoa-codex-hooks-status` | deferred | Native Codex hook registration or trust must be inspected for one selected root. | The runtime is not Codex or the question concerns archive content rather than hook state. | 1.0.0 | `adc1e110880d28ee` | | `aoa-codex-session-segment-archive` | deferred | A concrete Codex transcript must be archived or a missed archive event must be replayed. | The source is not a Codex transcript or the raw source cannot be read and verified. | 1.0.0 | `06f718756dbb22dd` | diff --git a/skills/port.manifest.json b/skills/port.manifest.json index 06df9931..578c920b 100644 --- a/skills/port.manifest.json +++ b/skills/port.manifest.json @@ -1,5 +1,5 @@ { - "schema_version": "aoa_skill_home_port_v2", + "schema_version": "aoa_skill_home_port_v3", "contract_ref": "aoa-skills:schemas/skill-home-port.schema.json", "owner_repo": "aoa-session-memory", "owner_ref": "skills/AGENTS.md", @@ -7,7 +7,7 @@ { "name": "aoa-session-memory-global-route", "path": "skills/aoa-session-memory-global-route", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "admitted", "visibility": "advertised", "admission_ref": "docs/decisions/AOA-SM-D-0018-owner-capability-home-and-skill-evidence-lifecycle.md" @@ -15,21 +15,22 @@ { "name": "aoa-session-memory-evidence-route", "path": "skills/aoa-session-memory-evidence-route", - "version": "1.0.0", + "version": "1.0.1", "lifecycle": "admitted", "visibility": "advertised", "admission_ref": "docs/decisions/AOA-SM-D-0018-owner-capability-home-and-skill-evidence-lifecycle.md" } ], - "exposure": { - "runtime": "codex", - "scope": "user", - "profile": "os-user-default", - "mode": "profile-selected", - "skills": [ - "aoa-session-memory-global-route", - "aoa-session-memory-evidence-route" - ] - } + "exposures": [ + { + "runtime": "codex", + "scope": "user", + "profile": "os-user-default", + "mode": "profile-eligible", + "skills": [ + "aoa-session-memory-global-route", + "aoa-session-memory-evidence-route" + ] + } + ] } - diff --git a/tests/test_install_skill_interface.py b/tests/test_install_skill_interface.py new file mode 100644 index 00000000..e49b403b --- /dev/null +++ b/tests/test_install_skill_interface.py @@ -0,0 +1,463 @@ +"""Focused tests for the bounded session-memory skill projection installer.""" + +from __future__ import annotations + +import json +from pathlib import Path +import shutil +import sys +from types import SimpleNamespace +from typing import Any + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) +import install_skill_interface as installer # noqa: E402 + + +SOURCE_IDENTITY = { + "status": "current", + "identity_status": "current", + "source_ref": "a" * 40, + "source_commit": "a" * 40, + "source_tree": "b" * 40, + "source_script": "scripts/aoa_session_memory.py", + "source_script_sha256": "sha256:" + "c" * 64, + "source_worktree_clean": True, + "diagnostics": [], +} +OWNER_CONTRACT = { + "owner_repo": "aoa-skills", + "schema_path": "schemas/capability_family.schema.json", + "schema_sha256": "d" * 64, + "validator_path": "scripts/validate_capability_home_port.py", + "validator_sha256": "e" * 64, + "validator_entrypoint_sha256": "f" * 64, + "contract_validator_path": "scripts/validation/validate_capability_home_port.py", + "contract_files": [], + "digest": "sha256:" + "1" * 64, +} + + +def _write_profile(workspace: Path, target: Path) -> bytes: + payload = { + "schema_version": "aoa_session_memory_install_profile_v2", + "artifact_type": "runtime_install_profile", + "installation_kind": "workspace_runtime", + "workspace_root": str(workspace.resolve()), + "aoa_root": str(target.resolve()), + "include_tests": True, + "install_id": "sha256:" + "2" * 64, + "source_ref": "3" * 40, + "source_commit": "3" * 40, + "source_commit_ref": "3" * 40, + "source_tree": "4" * 40, + "source_root": str(REPO_ROOT.resolve()), + "source_script": "scripts/aoa_session_memory.py", + "source_script_sha256": "sha256:" + "5" * 64, + "source_worktree_clean": True, + "source_identity_status": "current", + "installed_at": "2026-01-01T00:00:00Z", + } + path = target / installer._session_memory.INSTALL_PROFILE_PATH + path.parent.mkdir(parents=True, exist_ok=True) + raw = (json.dumps(payload, ensure_ascii=False, sort_keys=True) + "\n").encode() + path.write_bytes(raw) + return raw + + +@pytest.fixture +def overlay_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Path]: + """Build a target topology while keeping owner validation self-contained. + + The repository's graph intentionally names an external ``aoa-skills`` + contract. These filesystem/receipt tests exercise this installer without + making the standalone CI job depend on a sibling checkout; the real owner + validator is run by the integration lane. + """ + source = REPO_ROOT.resolve() + workspace = tmp_path / "workspace" + target = workspace / ".aoa" + target.mkdir(parents=True) + for relative in ("capabilities", "generated", "skills"): + shutil.copytree(source / relative, target / relative) + skills_root = tmp_path / "aoa-skills" + skills_root.mkdir() + profile_raw = _write_profile(workspace, target) + + sessions_sentinel = target / "sessions" / "sentinel.raw" + sessions_sentinel.parent.mkdir(parents=True) + sessions_sentinel.write_bytes(b"raw evidence stays\n") + maps_sentinel = target / "maps" / "index.json" + maps_sentinel.parent.mkdir(parents=True) + maps_sentinel.write_bytes(b'{"generated":"last-good"}\n') + sqlite_sentinel = target / "search.sqlite" + sqlite_sentinel.write_bytes(b"generated store\n") + hooks_sentinel = target / "hooks" / "sentinel.json" + hooks_sentinel.parent.mkdir(parents=True) + hooks_sentinel.write_bytes(b'{"hook":"untouched"}\n') + script_sentinel = target / "scripts" / "aoa_session_memory.py" + script_sentinel.parent.mkdir(parents=True) + script_sentinel.write_bytes(b"# target kernel producer sentinel\n") + unselected = target / "skills" / "aoa-session-batch-distill" / "SKILL.md" + unselected_raw = unselected.read_bytes() + + def source_provenance(_root: Path) -> dict[str, Any]: + return {**SOURCE_IDENTITY, "source_root": str(source)} + + monkeypatch.setattr(installer, "runtime_install_source_provenance", source_provenance) + monkeypatch.setattr( + installer, + "runtime_install_profile_status", + lambda *, root, workspace_root: { + "path": str(root / installer._session_memory.INSTALL_PROFILE_PATH), + "present": True, + "valid": True, + "include_tests": True, + "diagnostics": [], + }, + ) + monkeypatch.setattr( + installer, + "_run_owner_validation", + lambda _skills_root, _source_root: { + "ok": True, + "status": "validated", + "returncode": 0, + "stdout": "fixture owner validator\n", + "stderr": "", + "shared_contract": OWNER_CONTRACT, + }, + ) + return { + "source": source, + "skills": skills_root, + "workspace": workspace, + "target": target, + "profile_raw": profile_raw, + "sessions": sessions_sentinel, + "maps": maps_sentinel, + "sqlite": sqlite_sentinel, + "hooks": hooks_sentinel, + "script": script_sentinel, + "unselected": unselected, + "unselected_raw": unselected_raw, + } + + +def _args(env: dict[str, Path]) -> dict[str, Path]: + return { + "source_aoa_root": env["source"], + "skills_root": env["skills"], + "workspace_root": env["workspace"], + "aoa_root": env["target"], + } + + +def _mutate_selected(env: dict[str, Path]) -> None: + (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").write_text( + "old global package\n", + encoding="utf-8", + ) + (env["target"] / "skills/aoa-session-memory-evidence-route/SKILL.md").write_text( + "old evidence package\n", + encoding="utf-8", + ) + + +def test_check_install_preserves_kernel_runtime_and_records_exact_receipt( + overlay_fixture: dict[str, Path], +) -> None: + env = overlay_fixture + _mutate_selected(env) + before = { + key: path.read_bytes() + for key, path in ( + ("profile", env["target"] / installer._session_memory.INSTALL_PROFILE_PATH), + ("sessions", env["sessions"]), + ("maps", env["maps"]), + ("sqlite", env["sqlite"]), + ("hooks", env["hooks"]), + ("script", env["script"]), + ("unselected", env["unselected"]), + ) + } + + plan = installer.inspect_skill_projection(**_args(env)) + assert plan["ok"] is True + assert plan["status"] == "ready" + assert set(plan["selected_skills"]) == set(installer.DEFAULT_SKILLS) + + result = installer.install_skill_projection(**_args(env), force=True) + assert result["ok"] is True + assert result["status"] == "installed" + receipt_path = env["target"] / installer.RECEIPT_RELATIVE + receipt = json.loads(receipt_path.read_text(encoding="utf-8")) + assert receipt["schema_version"] == installer.SCHEMA_VERSION + assert receipt["selected_skills"] == list(installer.DEFAULT_SKILLS) + assert receipt["selected_paths"] == sorted(plan["selected_paths"]) + assert receipt["base_profile_sha256"] == plan["base_profile"]["sha256"] + assert receipt["base_install_id"] == plan["base_profile"]["install_id"] + assert receipt["graph"]["content_hash"] == plan["source_metadata"]["graph_source_content_hash"] + expected_packages = { + item["name"]: item + for item in plan["source_metadata"]["packages"] + } + assert {item["name"] for item in receipt["packages"]} == set(expected_packages) + assert { + item["name"]: item["version"] for item in receipt["packages"] + } == { + name: item["version"] for name, item in expected_packages.items() + } + assert all( + isinstance(item["fingerprint"], str) and len(item["fingerprint"]) == 64 + for item in receipt["packages"] + ) + for key, path in ( + ("profile", env["target"] / installer._session_memory.INSTALL_PROFILE_PATH), + ("sessions", env["sessions"]), + ("maps", env["maps"]), + ("sqlite", env["sqlite"]), + ("hooks", env["hooks"]), + ("script", env["script"]), + ("unselected", env["unselected"]), + ): + assert path.read_bytes() == before[key] + assert not list(env["target"].glob(f"{installer.STAGE_PREFIX}*")) + assert Path(receipt["backup_root"]).is_dir() + + current = installer.install_skill_projection(**_args(env), force=False) + assert current["ok"] is True + assert current["status"] == "already_current" + + +def test_install_requires_force_for_selected_difference(overlay_fixture: dict[str, Path]) -> None: + env = overlay_fixture + _mutate_selected(env) + before = (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").read_bytes() + result = installer.install_skill_projection(**_args(env), force=False) + assert result["ok"] is False + assert result["status"] == "force_required" + assert (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").read_bytes() == before + assert not (env["target"] / installer.RECEIPT_RELATIVE).exists() + + +def test_stale_base_profile_receipt_is_not_reported_as_already_current( + overlay_fixture: dict[str, Path], +) -> None: + env = overlay_fixture + _mutate_selected(env) + installed = installer.install_skill_projection(**_args(env), force=True) + assert installed["status"] == "installed" + receipt_path = env["target"] / installer.RECEIPT_RELATIVE + receipt = json.loads(receipt_path.read_text(encoding="utf-8")) + receipt["base_profile_sha256"] = "sha256:" + "9" * 64 + receipt_path.write_text( + json.dumps(receipt, ensure_ascii=False, sort_keys=True) + "\n", + encoding="utf-8", + ) + stale_raw = receipt_path.read_bytes() + result = installer.install_skill_projection(**_args(env), force=False) + assert result["ok"] is False + assert result["status"] == "receipt_stale" + assert "component_receipt_base_profile_sha256_mismatch" in result["diagnostics"] + + forced = installer.install_skill_projection(**_args(env), force=True) + assert forced["ok"] is True + assert forced["status"] == "installed" + refreshed = json.loads(receipt_path.read_text(encoding="utf-8")) + assert refreshed["base_profile_sha256"] == forced["plan"]["base_profile"]["sha256"] + assert refreshed["previous_component_receipt_sha256"] == installer._sha256_bytes(stale_raw) + + +def test_unselected_target_mismatch_fails_before_any_write( + overlay_fixture: dict[str, Path], +) -> None: + env = overlay_fixture + _mutate_selected(env) + env["unselected"].write_bytes(b"unselected drift\n") + profile_before = (env["target"] / installer._session_memory.INSTALL_PROFILE_PATH).read_bytes() + result = installer.install_skill_projection(**_args(env), force=True) + assert result["ok"] is False + assert result["status"] == "preflight_failed" + assert any("unselected_target_mismatch" in item for item in result["diagnostics"]) + assert (env["target"] / installer._session_memory.INSTALL_PROFILE_PATH).read_bytes() == profile_before + assert not (env["target"] / installer.RECEIPT_RELATIVE).exists() + + +def test_dirty_source_and_symlink_roots_fail_closed( + overlay_fixture: dict[str, Path], + monkeypatch: pytest.MonkeyPatch, +) -> None: + env = overlay_fixture + dirty = {**SOURCE_IDENTITY, "source_root": str(env["source"]), "source_worktree_clean": False} + monkeypatch.setattr(installer, "runtime_install_source_provenance", lambda _root: dirty) + result = installer.inspect_skill_projection(**_args(env)) + assert result["ok"] is False + assert "source_worktree_dirty" in result["diagnostics"] + + link = env["target"].parent / "target-link" + link.symlink_to(env["target"], target_is_directory=True) + result = installer.inspect_skill_projection( + source_aoa_root=env["source"], + skills_root=env["skills"], + workspace_root=env["workspace"], + aoa_root=link, + ) + assert result["ok"] is False + assert any("must not be a symlink" in item for item in result["diagnostics"]) + + +def test_compare_and_swap_conflict_does_not_restore_concurrent_target_edit( + overlay_fixture: dict[str, Path], +) -> None: + env = overlay_fixture + _mutate_selected(env) + concurrent = env["target"] / "skills/aoa-session-memory-global-route/SKILL.md" + + def edit_after_stage(_plan: installer.Plan) -> None: + concurrent.write_text("concurrent edit\n", encoding="utf-8") + + result = installer.install_skill_projection( + **_args(env), + force=True, + before_commit_hook=edit_after_stage, + ) + assert result["ok"] is False + assert result["status"] == "compare_and_swap_conflict" + assert concurrent.read_text(encoding="utf-8") == "concurrent edit\n" + assert not (env["target"] / installer.RECEIPT_RELATIVE).exists() + assert not list(env["target"].glob(f"{installer.STAGE_PREFIX}*")) + + +def test_failure_after_replacement_restores_only_owned_roots_and_receipt( + overlay_fixture: dict[str, Path], +) -> None: + env = overlay_fixture + _mutate_selected(env) + before = installer.inspect_skill_projection(**_args(env)) + before_global = (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").read_bytes() + + def fail_on_second(_relative: str, index: int) -> None: + if index == 1: + raise RuntimeError("synthetic replacement failure") + + result = installer.install_skill_projection( + **_args(env), + force=True, + replace_hook=fail_on_second, + ) + assert result["ok"] is False + assert result["status"] == "install_failed_rolled_back" + assert (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").read_bytes() == before_global + assert not (env["target"] / installer.RECEIPT_RELATIVE).exists() + assert not list(env["target"].glob(f"{installer.STAGE_PREFIX}*")) + assert before["ok"] is True + + +def test_receipt_publish_failure_restores_previous_receipt( + overlay_fixture: dict[str, Path], + monkeypatch: pytest.MonkeyPatch, +) -> None: + env = overlay_fixture + _mutate_selected(env) + first = installer.install_skill_projection(**_args(env), force=True) + assert first["status"] == "installed" + receipt_path = env["target"] / installer.RECEIPT_RELATIVE + prior_receipt = receipt_path.read_bytes() + # Make a second selected change and fail only the final receipt publish. + (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").write_text( + "second candidate\n", + encoding="utf-8", + ) + original_atomic_write = installer._atomic_write + + def fail_receipt(path: Path, raw: bytes, *, prefix: str) -> None: + if path == receipt_path: + raise OSError("synthetic receipt publish failure") + original_atomic_write(path, raw, prefix=prefix) + + monkeypatch.setattr(installer, "_atomic_write", fail_receipt) + result = installer.install_skill_projection(**_args(env), force=True) + assert result["ok"] is False + assert result["status"] == "install_failed_rolled_back" + assert receipt_path.read_bytes() == prior_receipt + assert not list(env["target"].glob(f"{installer.STAGE_PREFIX}*")) + + +def test_explicit_rollback_without_source_or_validator_restores_backup( + overlay_fixture: dict[str, Path], + monkeypatch: pytest.MonkeyPatch, +) -> None: + env = overlay_fixture + _mutate_selected(env) + before_global = (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").read_bytes() + before_evidence = (env["target"] / "skills/aoa-session-memory-evidence-route/SKILL.md").read_bytes() + installed = installer.install_skill_projection(**_args(env), force=True) + assert installed["status"] == "installed" + monkeypatch.setattr( + installer, + "runtime_install_source_provenance", + lambda _root: (_ for _ in ()).throw(AssertionError("source must not be read")), + ) + monkeypatch.setattr( + installer, + "_run_owner_validation", + lambda _skills_root, _source_root: (_ for _ in ()).throw( + AssertionError("owner validator must not be read") + ), + ) + result = installer.rollback_skill_projection( + workspace_root=env["workspace"], + aoa_root=env["target"], + ) + assert result["ok"] is True + assert result["status"] == "rolled_back" + assert (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").read_bytes() == before_global + assert (env["target"] / "skills/aoa-session-memory-evidence-route/SKILL.md").read_bytes() == before_evidence + assert not (env["target"] / installer.RECEIPT_RELATIVE).exists() + assert (env["sessions"]).read_bytes() == b"raw evidence stays\n" + assert (env["maps"]).read_bytes() == b'{"generated":"last-good"}\n' + assert (env["target"] / installer.ROLLBACK_OUTCOME_RELATIVE).is_file() + + +def test_rollback_rejects_tampered_backup_before_mutation( + overlay_fixture: dict[str, Path], +) -> None: + env = overlay_fixture + _mutate_selected(env) + installed = installer.install_skill_projection(**_args(env), force=True) + receipt = json.loads( + (env["target"] / installer.RECEIPT_RELATIVE).read_text(encoding="utf-8") + ) + backup = Path(receipt["backup_root"]) + stored = next(path for path in (backup / "paths").iterdir() if path.is_file()) + stored.write_bytes(b"tampered backup\n") + selected_after = (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").read_bytes() + result = installer.rollback_skill_projection(**_args(env)) + assert result["ok"] is False + assert result["status"] == "rollback_precondition_failed" + assert (env["target"] / "skills/aoa-session-memory-global-route/SKILL.md").read_bytes() == selected_after + assert installed["status"] == "installed" + + +def test_malformed_receipt_after_rows_are_not_current() -> None: + # Exercise the strict shape guard directly without constructing a full + # runtime fixture; this protects against ``all([])`` accepting malformed + # rows in future receipt readers. + plan = SimpleNamespace( + selected_roots=(Path("skills/example"),), + target_root=Path("/tmp/unused"), + ) + receipt = { + "selected_roots": ["skills/example"], + "selected_after": [None], + } + original = installer._selected_post_records + installer._selected_post_records = lambda _plan: ({"path": "skills/example", "state": "absent", "files": []},) # type: ignore[assignment] + try: + assert installer._receipt_after_matches(plan, receipt) is False + finally: + installer._selected_post_records = original diff --git a/tests/test_skill_behavioral_sandbox.py b/tests/test_skill_behavioral_sandbox.py index f1147526..39024de0 100644 --- a/tests/test_skill_behavioral_sandbox.py +++ b/tests/test_skill_behavioral_sandbox.py @@ -53,6 +53,13 @@ def _load_module(name: str, path: Path) -> Any: "aoa_session_memory_behavioral_source_tests", REPO_ROOT / "tests" / "test_session_memory.py", ) +doctor_tests = _load_module( + "aoa_session_memory_behavioral_doctor_tests", + REPO_ROOT / "tests" / "test_session_memory_doctor.py", +) +# Reuse the source owner's fixture through pytest's lifecycle, not a direct +# call to a decorated fixture or another copied install setup. +compact_install_source = legacy.compact_install_source def corpus() -> dict[str, Any]: @@ -142,6 +149,7 @@ def run_doctor_install_profile_case( *, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, ) -> None: skill_text = ( REPO_ROOT / "skills" / "aoa-session-memory-doctor" / "SKILL.md" @@ -160,6 +168,7 @@ def run_doctor_install_profile_case( "test_doctor_accepts_runtime_install_without_local_tests", tmp_path=tmp_path, monkeypatch=monkeypatch, + request=request, ) @@ -168,16 +177,28 @@ def run_legacy_case( *, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, ) -> None: - function = getattr(legacy, name) + source_tests = ( + doctor_tests + if name == "test_doctor_default_skips_deep_segment_index_event_parse" + else legacy + ) + function = getattr(source_tests, name) parameters = inspect.signature(function).parameters - unsupported = set(parameters) - {"tmp_path", "monkeypatch"} + unsupported = set(parameters) - { + "tmp_path", "monkeypatch", "compact_install_source" + } assert not unsupported, f"{name} requires unsupported fixtures: {unsupported}" kwargs: dict[str, Any] = {} if "tmp_path" in parameters: kwargs["tmp_path"] = tmp_path if "monkeypatch" in parameters: kwargs["monkeypatch"] = monkeypatch + if "compact_install_source" in parameters: + kwargs["compact_install_source"] = request.getfixturevalue( + "compact_install_source" + ) function(**kwargs) @@ -219,6 +240,7 @@ def test_controlled_skill_behavior_case( case: dict[str, Any], tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + request: pytest.FixtureRequest, ) -> None: sandbox_home = tmp_path / "home" sandbox_home.mkdir() @@ -243,6 +265,7 @@ def reject_network(*_args: Any, **_kwargs: Any) -> Any: run_doctor_install_profile_case( tmp_path=tmp_path, monkeypatch=monkeypatch, + request=request, ) else: prefix = "legacy:" @@ -251,6 +274,7 @@ def reject_network(*_args: Any, **_kwargs: Any) -> Any: runner.removeprefix(prefix), tmp_path=tmp_path, monkeypatch=monkeypatch, + request=request, ) assert sandbox_home.is_dir()