CI Gates - alexherrero/agentm GitHub Wiki
This page lists every gate in the deterministic battery. It explains the invariant each gate proves and identifies the script behind it. Three per-OS Python-stack workflows run in parallel, alongside a fourth Go-daemon workflow with its own three-OS matrix (see the Quick Reference table and the aggregate note below). Run the deterministic subset locally in one command before you commit:
bash scripts/check-all.shWorktree-native flow (ratified 2026-07-06): the full battery gates on pull_request, not on a plain push to main. The main branch only merges a PR after its required checks pass. A plain push to main runs almost nothing. In tests-linux.yml, the single syntax job (bash -n and AST-parsing every .ps1) runs unconditionally. Every other job in all three per-OS workflows gates on github.event_name == 'pull_request', workflow_dispatch, or the nightly workflow_call. This includes the install-smoke job in both tests-mac.yml and tests-windows.yml. The [T] CI All aggregate badge workflow carries no push trigger at all. It runs only on pull_request. The pull_request trigger itself carries no paths-ignore filter. A trigger-level filter that skips a required check would block the merge of a docs-only PR forever. The docs/wiki diet returns as a job-level no-op instead.
The aggregate polls four workflows now, and a name missing from that list gates nothing. [T] CI All's aggregate job doesn't run tests itself β it polls tests-linux, tests-mac, tests-windows, and, since 2026-09-05, daemon (the Go daemon's own three-OS matrix, in the Quick Reference table below), requiring success from each before it reports green. Before that date daemon.yml existed but wasn't in the polled list, so its result was invisible to the merge gate: PR #554 merged with the Go suite red because the one required check never looked at it. scripts/test_ci_consistency.py (see check-ci-consistency below) now pins the polled workflow list, so a workflow can't go back to silently gating nothing.
| Workflow | Runs on | Jobs |
|---|---|---|
[T] Linux Tests |
ubuntu-latest |
install-smoke + adapter-parity + validate + check-references + check-wiki + unit tests + verify-v4 + check-vendored-parity (vault-lock + hook-config modes) + check-storage-rules + check-payload-layout-free + check-payload-parity + check-no-title-case-roots + check-no-harness-paths + check-vocabulary-membership (self-test) + check-vault-frontmatter (self-test mode) + check-project-yaml (self-test) + verify-orchestration-briefing + verify-idle-chain + verify-hook-resolution + verify-state-routing + verify-reflection + verify-dreaming + check-dreaming-parity + verify-declare-a-type + verify-auto-org-meters + health-score-determinism + validate-audit-coverage + run-ablation-baseline + verify-phases + verify-memory-roundtrip + syntax + lib-parity (check-vendored-parity's lib mode) + pii-guardrails (check-no-pii + gitleaks) + dogfood-workflows |
[T] Mac Tests |
macos-latest |
install-smoke + validate + check-references + unit tests + verify-v4 + check-vendored-parity (vault-lock + hook-config modes) + verify-orchestration-briefing + verify-hook-resolution + verify-state-routing + verify-reflection + verify-dreaming + verify-declare-a-type + verify-auto-org-meters + health-score-determinism + validate-audit-coverage + run-ablation-baseline + verify-phases + verify-memory-roundtrip + syntax (both shells) + lib-parity (check-vendored-parity's lib mode) + check-no-pii |
[T] Windows Tests |
windows-latest |
install-smoke (pwsh) + installer-boundary + validate + check-references + pwsh syntax + lib-parity (check-vendored-parity's lib mode) + check-vendored-parity (hook-config mode) + check-no-pii + unit tests |
[T] Daemon (Go) |
ubuntu-latest + macos-latest + windows-latest (one workflow, a three-OS matrix) |
go.mod/go.sum tidy check + gofmt + go vet + build + go test ./... (covers check-dreaming-parity's own tests too) + agentmd/agentmdream fail-clean-with-no-vault checks |
| Gate | Invariant | Script |
|---|---|---|
| install-smoke | Fresh install succeeds; re-run is idempotent; --update refreshes managed files but preserves user edits to wiki/ and AGENTS.md; test infra never propagates to scratch. |
scripts/smoke-install-bash.sh, scripts/smoke-install-pwsh.ps1
|
| post-install integrity | Hook-command paths resolve; every .sh/.ps1 parses; bash installer produces bash commands, pwsh installer produces pwsh commands; settings.json has the expected schema; .harness state files are valid. |
scripts/check-integrity-bash.sh, scripts/check-integrity-pwsh.ps1
|
| adapter-parity | Every adapter ships the canonical set of phase-commands, sub-agents, and skills. | scripts/check-parity.sh |
| validate | Every TOML, YAML frontmatter, and JSON across adapters/ and templates/ parses and has required keys. |
scripts/validate-adapters.py |
| check-references | Every harness/<phases|agents|skills|pipelines>/*.md mentioned in an adapter file exists; phase-spec "dispatch the <name> sub-agent / invoke the <name> skill" lines point at a canonical spec; settings-fragment-{bash,pwsh}.json have matching schemas. |
scripts/check-references.py |
| check-wiki | DiΓ‘taxis structural rules (aβk): mode purity, ADR append-only + Status: accepted|superseded|rejected, orphan-link detection, globally-unique filenames, no banned-headings-per-mode. Runs --strict (blocks PRs) when wiki/.diataxis is present; warn-only otherwise. Shipped in v0.9.0 as part of the DiΓ‘taxis rollout (Foundations HLD). |
scripts/check-wiki.py |
| check-slop | The deterministic anti-slop voice gate over wiki/, --strict (blocks on error-/warning-tier findings; suggestion-tier never fails). Mechanism-owned by crickets (voice is a crickets-side capability) β this agentm-repo script is a thin delegator that locates the crickets sibling checkout and shells out to its real check-slop.py + the one shared rule pack (src/wiki/skills/diataxis-author/style/voice-rules.json), so agentm doesn't carry a second copy of the scanning logic. No direct CI step of its own β runs via the UNIT_WRAPPED allowlist (test_check_slop.py subprocess-invokes it), same pattern as check-ci-consistency. |
scripts/check-slop.py |
| check-kind-taxonomy | Reports every distinct raw type: or kind: value in the corpus, its frequency, and kebab-case violations. It prints check-vocabulary-membership.py --report, so it reads the notes that gate resolves: the whole vault root, found through $MEMORY_ROOT when exported and the configured vault otherwise, with a graceful skip (exit 0) only when nothing resolves. Until 2026-09-20 it read $MEMORY_ROOT alone and skipped on every battery run. Report-only, never a failing gate β the real vault's 47-value mess can't be a hard PASS/FAIL yet without an operator normalization call (parked as agentm issue #273), so the step's own exit code is always 0 regardless of findings (the same report-only shape check-slop.py had before it flipped to --strict in check-all.sh β Consolidation arc CONS-3). See Kind-taxonomy registry. |
scripts/check-kind-taxonomy.sh β scripts/check-vocabulary-membership.py β harness/skills/memory/scripts/kind_registry.py
|
| syntax |
bash -n on every .sh; PowerShell AST parse on every .ps1 across repo root + scripts/ + templates/ + adapters/. |
scripts/check-syntax.sh, scripts/check-syntax.ps1
|
| unit tests | Every scripts/test_*.py (auto-discovered) passes β the memory-script logic in isolation. Since filing-v2 part 2a moved machine state to ~/.local/state/agentm/, the runner is hermetic on that seam: run_unit_suite.py is unittest discovery plus a fresh scratch directory per test for every variable scripts/engine_state_isolation.py governs ($AGENTM_STATE_DIR, $XDG_CACHE_HOME, $AGENTM_RECALL_HISTORY, $AGENTM_DEVICE_LOCAL_ROOT), and scripts/conftest.py gives pytest invocations the same guard β without it, tests would read and write the machine's real engine state, a test that reaches prompt_submit() would append to the recall ledger at ~/.cache/agentm/telemetry/recall-history.jsonl, which sits outside the state directory, one that rebuilds a graph snapshot would write the operator's ~/.agentm/memory/_meta, and one that reaches the runner's job records by their default would read and write the live runner's own under ~/.cache/agentm/runner, which follows $XDG_CACHE_HOME. Every verify-* script exports its own scratch $AGENTM_STATE_DIR script-wide for the same reason. The seven gates that lock a fixture vault also export a scratch $XDG_CACHE_HOME, because every vault vault_mutex locks leaves a hash-named directory under ~/.cache/agentm/locks: verify-idle-chain, verify-state-routing, verify-reflection, verify-phases and verify-memory-roundtrip lock directly, and validate-audit-coverage and run-ablation-baseline lock through the verify scripts they drive. test_engine_state_not_leaked runs each of the seven under a throwaway home and checks that the default lock root gains no entry. A run leaves nothing in the temporary directory: the runner keeps its directories under one agentm-unit-* directory there, removes each test's once the next test has moved the variables off it, and removes the whole directory when the run stops, so a run that is killed leaves one directory and not one per test. test_unit_suite_leaves_nothing drives the runner over a probe suite under a scratch TMPDIR and checks a finished, an interrupted and a killed run. |
(cd scripts && python3 run_unit_suite.py) |
| check-vendored-parity | One parametrized gate over six vendored/checksum-manifest parity invariants (CONS-1 merge of what were five separate scripts, plus the L6 wiki-publish-render addition β lib, vault-lock, wiki-publish-transform, storage-seam, hook-config, workflow modes): (1) lib β lib/install/ matches its committed checksums; (2) vault-lock β scripts/vault_lock.py and its vendored twin harness/skills/memory/scripts/vault_lock.py are sha256-identical, so the memory skill and the harness core share one canonical lock implementation (testInfra#0); (3) wiki-publish-transform β scripts/wiki_publish_transform.py (agentm's own dogfooded copy) and templates/scripts/wiki_publish_transform.py (the copy install.sh vendors into every installed project) are sha256-identical; (4) storage-seam β the same vendoring pattern for storage_seam.py + storage_device_local.py; (5) hook-config β two functions replicated verbatim across the four memory hooks are byte-identical, so a fix applied to only one hook is caught as drift: _resolve_vault_path (env β .agentm-config.json plugin key β legacy flat key β none; R0.1 / agentmEngine#0) and _resolve_agentm_python (the bootstrap that hands the memory scripts a working interpreter β the probe itself lives once, in harness/hooks/lib/resolve-python.sh, so only the few lines that find and call it are replicated, and gating them stops a hook drifting back to a bare python3 the way the whole set had done); (6) workflow β every templated templates/.github/workflows/*.yml is active at the repo root, byte-identical (the local mirror of the dogfood-workflows CI job, below). Runs all six by default; bash scripts/check-vendored-parity.sh <mode> runs just one. The vault-lock and hook-config modes run as direct steps in tests-linux.yml/tests-mac.yml/tests-windows.yml; lib runs as its own lib-parity job. |
scripts/check-vendored-parity.sh |
| check-multi-plan-naming | Locks the named-plan naming contract three ways: (1) scripts/harness_memory.py still exposes the named-plan resolver surface β both resolve_active_plan (the sessionβplan binder) and state_dir (the state-dir enumerator, renamed from harness_state_dir under agentm-vault plan 15); (2) no curated harness/*.md doc hard-asserts a singleton via the narrow deny-pattern β definite-article the PLAN.md + possessive PLAN.md's β which still permits every legitimate mention (a named PLAN-<name>.md, a PLAN*.md glob, a <slug>.PLAN.md queued file, the vault-state-path PLAN.md CLI example, and PLAN.archive.*); (3) both session-start hook twins (harness-context-session-start.{sh,ps1}) still glob PLAN-*.md, so they cannot drift apart and silently lose named-plan discovery at session boot (assertion 3, added V5-10 part 1 task 5). Scans 7 curated docs; design/SKILL.md is included as a regression guard. See Named plans. |
scripts/check-multi-plan-naming.sh |
| check-worktree-slug | The worktree slug-safety invariant (V5-10 / LC-2): a worker in a git worktree can't see the parent's gitignored .harness/, so it resolves the vault slug by the origin basename alone (Tier 3). If the full-chain slug (an explicit vault_project / github.repo override) diverges from the origin basename, a worktree worker would silently write plans/progress under the wrong projects/<slug>/. Delegates to vault_project.py check-worktree-slug (the same resolver the doctor probe calls, so gate + probe never drift); no origin remote β warn-only. |
scripts/check-worktree-slug.sh |
| check-no-auto-worktree | No agentm automation surface auto-spawns a worktree (V5-10 / LC-3): worktrees are an operator-initiated primitive (the spawn helper lives crickets-side). Scans executable surfaces (shell Β· python Β· pwsh Β· CI yaml) for the git worktree add spawn verb; read/cleanup subcommands (list/remove/prune) are allowed, tests + this gate's own file excluded. Proves agentm itself never creates a worktree unprompted. |
scripts/check-no-auto-worktree.sh |
| check-one-way-imports | One config-driven gate over five one-way import-direction invariants (CONS-1 merge of what were three separate scripts, converged onto Python β opinion-resolver, capability-resolver, process-seam, lc8-storage-vault, lc8-bridge rules): (1) opinion-resolver β opinion_resolver.py never imports plugin code; (2) capability-resolver β capability_resolver.py/capability_version_match.py are one-directional (V5-8 Task 4), reading plugin manifests as data, never importing plugin code at runtime; (3) process-seam β the memoryβprocess edge is one-directional (V5-4 / LC-4): the process-seam client (scripts/process_seam.py) imports the memory engine, never the reverse β scans agentm's Python automation surfaces for any module importing process_seam (AST module-name equality, not a regex β a same-prefix sibling like process_seam_helper can never be mistaken for the real module), excluding test_*.py and process_seam.py itself; (4) lc8-storage-vault β the de-vaulted routing files (harness_memory.py, repo_registry.py) never import the storage_vault capability plugin directly (LC-8); (5) lc8-bridge β kernel toolkit scripts under harness/skills/memory/scripts/ never import harness_memory back (V5-5 LC-8 bridge extension). Runs all five by default; --rule <name> runs just one. Runs cross-OS via the auto-discovered unit tests step (subprocess tests in test_process_seam.py, test_check_opinion_resolver_one_way.py, test_check_capability_resolver_one_way.py), so no separate per-OS workflow step exists. See Memoryβprocess seam, Process seam, and Orchestration bridge. |
scripts/check-one-way-imports.py |
| check-storage-seam-no-path-leak | No pathlib.Path crosses the memoryβstorage seam (V5-1): every seam verb returns the seam's own Locator/Info, never a path, so a filesystem assumption can't reach the engine. Static (AST, not grep): parses each scripts/storage_*.py and flags any seam verb (resolve/read/write/list/exists/info/mkdir) whose return annotation references a path type, however nested (Path, list[Path], `Path |
None, os.PathLike). Internal Pathuse is fine β a filesystem backend'sroot / key; only handing one back is the leak. test_*.pyis out of the glob, so conformance fixtures (which build aPath-returning backend to test the gate) don't trip it. The structural sibling of check-one-way-imports's process-seamrule; runs cross-OS via the auto-discoveredunit testsstep (subprocess tests intest_storage_seam.py`). See Memoryβstorage seam and Storage seam. |
| check-opinion-honesty | No orphan Opinion references (agentm-opinion-registry.md Enforcement Β§3): every consumer-declared Opinion name must resolve to a real opinions/<name>.md entry. Today the only consumer-declared field this checks is a persona's opinions: manifest key, which is itself [PENDING-IMPL] β so the gate is almost always a quiet no-op until personas start declaring opinions, which is correct, not a bug. No direct CI step of its own β runs via the UNIT_WRAPPED allowlist (test_check_opinion_honesty.py subprocess-invokes it), same pattern as check-ci-consistency. |
scripts/check-opinion-honesty.py |
| check-personas | Every kind: persona manifest under personas/ keeps requires: β substrate-native scripts (no hard dependency on a crickets capability) and never sets always_load/always-load to true (both spellings checked) β see persona-tier-schema reference. |
scripts/check-personas.py |
| check-governs-index | Every governs:/area: stamp in wiki/designs/ resolves cleanly against governs_resolver.build_index(): no two designs claim the same governs: pattern (overlap), and every area: value is in the canonical AG taxonomy (unknown-area) β catches an authoring mistake at commit time instead of a grounding hook silently treating the file as greenfield (R0.10). |
scripts/check-governs-index.py |
| check-no-pii | The regex PII scanner finds no personal info across the tree (this is a public repo). | scripts/check-no-pii.sh |
| check-retrieval-regression | The shipped ranker has not regressed against a pinned baseline on the frozen gold set. The nightly job (scripts/health/retrieval_gate_job.py) runs agentmd embed before the gate and records it in latest_retrieval_gate.json (task 182), so a day of moves no longer turns the night into a SKIP; the eval's canary carries instrument: eval-canary and lifecycle: pinned, which keep enrichment and the decay curve off it. Its measurement contract β resolution, provenance, pre-registration rules β is Retrieval-Eval-Contract. It runs eval_retrieval_shipped.py, which issues each query exactly as the recall hook does β the same Γ2 over-fetch, _daemon_admissible filtering, and temporal bounds β against agentmd search, the ranker anyone actually queries, and compares per-question hit/miss against scripts/health/fixtures/week1-gold/shipped-baseline.json with an exact paired test. Three states rather than two: PASS, FAIL, and SKIP when there is no pinned baseline or the corpus is cold. The skip is deliberate and narrow: the eval refuses to report on a cold embedder or above 5% stale vectors, because a lexical-only run reported as a hybrid number is worse than no number. A FAIL now has two distinct causes: the ranking regressed, or the comparison was refused (COMPARISON REFUSED, exit 3) because the baseline carries no corpus fingerprint (documents, embedded-in-scope count, gold-set hash, pin date) or names a different gold set β the gold-set check is never overridable.734. This script always passes --drifted-ok: it is the standing tripwire against the live vault, so ordinary corpus movement prints beside the verdict instead of refusing. This gate exists because the eval it replaces had sat behind a promotion criterion nothing read β anything shipped shadow-first here names its reader in the same change. Since filing v2 part 3 the gold set's pinned pre-migration paths are translated, note by note, through the migration's own disposition reports (<memory root>/diagnostics/migrations/corpus-migration-3/*/dispositions.csv, later runs overriding earlier) and the operator's whole-tree moves are exact prefix remaps beside 2b's; a path no report moved compares as it is, and a machine without reports runs the pre-migration comparison. A basename fold was tried first and rejected in the part-3 pre-tag review β it would have called two notes that shared a name in different month buckets the same note. Against the migrated corpus the gate reads 0.734 β 0.688 (one flip to a hit, four to a miss, exact paired p = 0.375, no significant regression); two of the misses are the primos questions, whose records moved to Projects/primos/ and carry the ranker's project-space penalty. |
scripts/check-retrieval-regression.sh |
| check-storage-rules | The filing contract parses, and the taxonomy has not grown without a warrant. standards/storage-rules.md steers filing at runtime, which makes a typo in it the newest silent-failure surface in the system β a model handed a malformed rule does not stop, it improvises, and the filing that results looks fine and is wrong. Two assertions. parse: the shipped contract embedded in the daemon parses and validates, always; the live vault instance does too when a vault resolves. A vault with no rules file is a documented skip, because absence falls through to the embedded copy by design; a vault whose rules file is corrupt is a failure. growth rule: a diff that adds a value to memory_types carries its warrant in the same diff β the query class that needs it, the nearest existing type, and why that one does not fit β machine-checked against the block's own warrants map. Fifty-odd values accumulated in the old taxonomy because every addition was individually defensible and nothing ever asked whether the set still cohered; this is the brake that asks. Unlike check-kind-taxonomy, which is report-only because the live corpus has known data-quality problems a hard gate would block unrelated work on, this gate fails the battery: it guards one file, and it is the file everything reads. The parse itself is done by the daemon (agentmd rules), which is the only parser of that file β see Memory daemon Β§ the filing contract. |
scripts/check-storage-rules.py |
| check-vocabulary-membership | No note enters the corpus carrying an unregistered type: or kind: value β the enforcement gap the Filing v2 census found: the type-XOR-kind exclusivity rule held at zero violations while kind: alone had drifted to 38 in-use values against a 32-value register, because only the exclusivity was ever checked, never the values themselves. Two rules close it. A collision check inside the daemon's own parser fires on every parse and refuses any word claimed by both memory_types and record_kinds. A membership audit over the live corpus applies set-ratchet semantics: a recorded baseline stores every currently-tolerated offender as a (path, value) pair, a swap can't hide inside a stable count, any offender missing from the baseline fails the battery immediately, and the baseline only ever shrinks. Retired values (named in the contract's own deprecations map) sit in a separate migration-pending bucket, outside the offender count. --strict drops the baseline and fails on any offender at all β the mode check-all.sh's invocation runs, the corpus migration (filing v2 part 3) having drained the baseline to empty. Two halves, two places: a CI runner has no vault, so on tests-linux.yml the corpus half skips itself with a named line and the self-test is what runs β the collision fixture the parser must refuse, and a scratch-vault unregistered value the audit must catch; the corpus audit runs locally through check-all.sh, where the real corpus is. It finds the corpus the way the sibling gates do: $MEMORY_ROOT (or its deprecated alias) as the override, else the configured memory root (harness_memory.memory_root()). It skips when neither resolves, and when the export names no directory, rather than falling through to the configured vault. It walks the whole vault root that memory root sits in, the way the daemon's index does, never entering the contract's recall_exempt_areas, and its scope: line names each top-level directory walked with the notes read in it. The live first run recorded a zero-offender baseline over 212 retired-value notes awaiting the migration. Until 2026-09-20 the battery's run read nothing. The gate resolved $MEMORY_ROOT alone and check-all.sh has never exported it, so from its first day the corpus half printed a skip line and passed. The 0 offenders over 2,291 notes this row once reported came from a hand run on 2026-09-03, over a hand-listed walk that by 2026-09-20 reached only agent/memory/ and projects/, missing standards/, calendar/, personal/ and the root notes. Widened and resolved, it read 2,951 notes on 2026-09-20 and failed on 12 unregistered values in the operator's own notes: plan Γ8, how-to Γ2, progress Γ1 and proposal Γ1. Registering a value is an edit to standards/storage-rules.md, which the operator owns, so the gate stays red until they rule; --strict was not loosened to pass them. --report prints the same audit in full and always exits 0; check-kind-taxonomy.sh runs that mode. |
scripts/check-vocabulary-membership.py |
| check-payload-layout-free | The pasted context payload names no folder a migration moves. templates/agentmemory-context.md is the text pasted into claude.ai and the Gem, and until 2026-09-07 it carried a folder map β which is the one thing every landing group of the vault series changes, and the reason three chat surfaces served a July layout for two months. A surface that looks for a folder that no longer exists finds nothing, says nothing, and answers from its own knowledge, which is the failure the payload exists to prevent. The gate scans the template for nine names the series retires (Agent/, Projects/, _inbox, _always-load, _index.md, _harness, Agent/desk, Agent/_meta, and filing_confidence: low as the unfiled marker) and fails on any of them, leaving the payload to name the three things that do not move: index.md, standards/ and moc-projects.md. The scan is case-sensitive on purpose β Agent/ is the capitalized root the casing rename retires, while agent/archive/ is the lowercase path the card guide is required to explain. The predecessor template is kept at scripts/fixtures/agentmemory-context.september.md so test_payload_surface.py can show the gate rejecting all nine; a checker that has never rejected anything is not evidence. |
scripts/check-payload-layout-free.py |
| check-payload-parity | Every copy of the context payload is the template's derivation rather than a hand-kept variant. templates/agentmemory-context.md is the source; scripts/payload_render.py is the one renderer; two copies come out of it β the tracked Antigravity rule at adapters/antigravity/rules/agentmemory-context.md, always rendered without a capture address because the operator's mailbox is not something to commit, and the AGENTMEMORY managed section of ~/.gemini/GEMINI.md, rendered with the address when one is configured. The gate compares each present copy to its render by sha256 and prints the two hashes when they differ. Both copies were hand-kept until 2026-09-07 and both had drifted: the rule and the template had become different documents, and Gemini's copy was still serving a July folder map. The Gemini half graceful-skips when there is no ~/.gemini/GEMINI.md β every CI runner, and every machine without Antigravity β while the Antigravity half is a tracked file and never skips. Repair is python3 harness/skills/memory/scripts/payload.py --write, which the doctor's payload rows also name. |
scripts/check-payload-parity.py |
| check-vault-frontmatter | Every note in the vault has frontmatter that parses as a YAML mapping. Three classes: parse-error β yaml.safe_load raises, usually on an unquoted scalar holding a colon-space, which YAML reads as a nested mapping and rejects; not-a-mapping β the block parses, but to a list, a bare scalar, or nothing; truncated-value β an unquoted scalar holds a #, so YAML ends the value at the comment and drops the rest without raising. Four more semantic checks ride the same parse, over the parsed frontmatter rather than its shape: missing-provenance β a kind: crystallized note with no non-empty consolidated_from: list; missing-successor β lifecycle: superseded without a superseded_by: naming the successor; inverted-supersession β a superseded memory, on either the lifecycle or the pre-contract status axis, still carrying supersedes: (the pointer running the wrong way β the successor carries supersedes:, the superseded note carries superseded_by:); source-not-a-transport β a source: value that is not one of the contract's closed transports (operator-direct, conversation, external-fetch, inbox), naming which field the value belongs in instead β source_url: for a fetched page's address, source_id: for anything else. The transport list is written into the gate rather than read from the contract, because this gate runs on CI runners with no vault and no resolvable contract, and a rule that stops checking when the contract can't be found is a rule that stops checking on every runner. A copy can drift, though: plan 16 traded email for inbox in the contract alone, so test_check_vault_frontmatter.py now holds the list to the packaged contract's sources: block. Nothing else in the stack parses frontmatter strictly: frontmatter_validator.py and vault_lint.py are stdlib-only by design and split key: value on the first colon, and the Go daemon's splitFrontmatter is a regex split. This gate deliberately does not inherit the _EXCLUDE_DIRS frozenset both linters carry, because that set holds the notes most likely to break β long-prose status:/inputs: fields β and used to hold _harness/, where the design and handoff memos lived then; agentm-vault plan 15 retired _harness/ from that set in favor of a project's tasks/<task>/ directory, the same kind of load-bearing-prose content in its new home. Nine notes carried unparseable frontmatter for up to two months under green CI before the gate existed; the first run of the gate found two more, both silent truncations under _harness/ (as it then was), one of them losing 213 of 261 characters. A # that opens a conventional comment (a space follows it, or it ends the line) is left alone, and every truncation is confirmed against the parser before it is reported β the scanner predicts the prefix YAML would keep and reports only when safe_load returned exactly that. Two modes, because a CI runner has no vault: check-all.sh runs the scan over the resolved vault (harness_memory.vault_path(), graceful-skip when nothing resolves), while CI runs --self-test, which builds a scratch vault of known-bad notes and asserts each defect at the expected file:line. The scan root is the vault root, not the memory root. Pinned to memory_root() from 2026-08-09 to 2026-09-20, the scan shrank without a word as the vault grew beside agent/ (filing v2 moved projects/ out, and standards/ and calendar/ were created at the vault root), until it parsed 446 of 2,975 notes and reported them clean. The report's scope: line now names each top-level directory walked and its note count, so a narrowed root shows in the output. The one space it never enters is the recall wall: the contract's recall_exempt_areas, the folder holding certificates and recovery codes. A walled directory is pruned from the walk and a walled file is never opened, the rule the daemon's index keeps, because a finding quotes the block it came from and a broken note there would print its frontmatter into every check-all run. From the widening until this rule, the scan opened the 14 notes there. The areas are read once per scan and fall back closed to the shipped contract's list when no contract resolves. They are named from the vault root, so a scan rooted below it with --vault is placed under the resolved vault root first. test_check_vault_frontmatter.py drives the scanner against scratch vaults on all three operating systems via the auto-discovered unit-test step. |
scripts/check-vault-frontmatter.py |
| check-memory-root-shape | The memory root has the shape the memory-root trims (agentm-vault plan 05) left it in: agent/ holds only diagnostics/ and memory/ (plus archive/ once plan 11 creates it, and Home.md loose until the maps data run writes memory/.maps-and-root-notes-complete β agentm-vault plan 07 retires it, but only from that marker on, so the retired map cannot come back) β no engine JSON, no _meta/, no _dream/, no desk/; memory/ holds only the six class directories, no pen, no watchlist, no settings file; standards/ holds storage-rules.md, user-preferences.md, security-and-secret-governance.md, moc-standards.md, and the voice library under voice/. Resolved at runtime through vault_layout.py, never a literal. Three states, not two: pre-migration β every retired location still holds its files and none of the new ones exist, so the moves haven't run β reported and exit 0, because failing every build until the operator runs scripts/migrate/memory_root_trims.py --apply would gate nothing; migrated β nothing retired remains, and the shape is enforced in full; mixed β both at once (a half-applied move, or a writer that recreated a retired path) β named item by item, exit 1. Every data run's marker in memory/ is on this gate's ignorable-litter list, so none counts as a stray file: the card backfill's (.card-backfill-complete, below), the maps data run's (.maps-and-root-notes-complete), the root casing's (.root-casing-complete), the projects migration's (.projects-migration-complete) and the ideas move's (.ideas-surface-complete). |
scripts/check-memory-root-shape.py |
| check-card-shape | Every note in the class directories other than mocs/ (generated navigation; agentm-vault plan 07 owns its shape) has the card's shape (agentm-vault Β§ The card). A card (type:) carries title, status, lifecycle, filing_confidence, source, trust, created and updated β a card no pass has judged yet (no enriched_at) may wait for the deep pass to write its title, and the daemon's self-probe names no transport; its fields are in the card's order; it carries no retired field; trust is the contract's tier for its source; filing_confidence is high exactly where confidence is at or above the contract's floor; it is never active at low; lifecycle is on the contract's axis (pinned included); slug equals the file name; and the file name follows the naming rule (a name, never a counter). A record (kind:) keeps the card's order for the fields it shares, carries no retired field, never carries importance, why, filing_confidence or trust, and names what it touched touched:, not entities:. A second walk reads the idea cards (agentm-vault plan 13): every markdown file directly in the vault root's personal/ideas/, no dotfile and nothing in a subfolder. An idea card is type: idea and carries an area:, the operator's own group name from an open set. A status, when it carries one, is active; a card without one is read as active, and the night writes the field. A dismissed:, when it carries one, is a YYYY-MM-DD day. It never carries lifecycle or lifecycle_since, its fields are in the card's order, and it carries no retired field. A note there that is not type: idea is a finding too. The class card's other rules stop short of the folder on purpose: they require lifecycle, and they hold filing_confidence to the floor, while the night keeps an idea's filing whatever the score. Every finding names the file and the field. Resolved at runtime, over the live vault. Two states: pre-backfill β every finding the card backfill would clear is reported and the gate exits 0, because failing every build until the operator runs scripts/migrate/card_backfill.py --apply would gate nothing; backfilled β once memory/.card-backfill-complete exists, every finding fails. The idea walk has two states of its own: report only β each finding is listed and the gate exits 0; enforced β once memory/.ideas-surface-complete exists, every finding fails. The ideas move writes that marker from scripts/migrate/ideas_migration.py --finish, which runs the same walk and writes it only when the folder holds at least one card and none has a finding. |
scripts/check-card-shape.py |
| check-card-prompt |
templates/inbox-card-prompt.md β the paste that tells a chat surface how to write a card into agent/inbox/, which today means claude.ai, because Gemini cannot create a file in Drive β teaches the shape the card actually has. Every field in its worked example is a real card field (card_shape.READ_ORDER / MACHINE_ORDER), none is a retired one, the example is in the card's own order, status and trust are exactly unfiled and untrusted so a chat surface cannot file by writing a word, and agent/inbox/ is the only write target it names. A paste has no CI of its own; this is the CI it gets. Self-tests against four drifted variants. |
scripts/check-card-prompt.py |
| check-memory-skill-table | The /memory sub-command table at the top of harness/skills/memory/SKILL.md names what the skill actually does β nothing generates it, so it drifts the way an unchecked list always drifts (the line above it once read "four sub-commands" through roughly twenty additions, search a stub for most of them). Checks both directions: a table row with no matching ### /memory <verb> section (promises something the doc never explains), and a section with no row (crystallize sat undiscoverable behind a working implementation this way). A fixed OFF_TABLE set names the sections deliberately left out of the table and why (diary, promote), so "not in the table" reads as a decision rather than a hole. Plan 12 built the gate and never wired it into CI; the agentm-vault arc's close-out orphan census found it with no caller β it read clean the moment it was wired (#690). |
scripts/check-memory-skill-table.py |
| check-class-directories | Each of the six class directories under memory/ holds only a card or a record β a note carrying exactly one of type:/kind:, with a value the filing contract registers β and nothing else: no subdirectory, no non-note file, no note without a frontmatter block. Resolved at runtime, over the live vault. Two states: pre-backfill β reported and exit 0 (the purge left six now-empty opinion-lane directories inside crystallized/, which the backfill removes); backfilled β once memory/.card-backfill-complete exists, every finding fails. mocs/ gets a second, additive shape check on top of that base rule (agentm-vault plan 07): it may hold only the three named maps (moc-root, moc-memory, needs-review), the class index, and a page per memory type with at least moc_min_members live notes β a numbered page (<type>-2.md) is now a finding, since a type's page paginates inside itself instead of splitting into a second file. live_type_counts (maps_shape.py) counts members the way the mocs job's own mocMembers does, so the gate and the job agree on which types have earned a page. This half runs on its own marker, independent of the card backfill's: reported until the maps data run writes memory/.maps-and-root-notes-complete; enforced from then on. |
scripts/check-class-directories.py |
| check-no-empty-tags | No note across the six class directories carries an empty tags list β tags: [], a quoted empty string, or a bare tags: with nothing under it (agentm-vault Β§ The card: a card with no tags omits the field rather than writing an empty one). Resolved at runtime, over the live vault. Same two states: pre-backfill β counted and exit 0; backfilled β once memory/.card-backfill-complete exists, any empty list fails. |
scripts/check-no-empty-tags.py |
| check-root-notes | The root notes the maps retired stay retired (agentm-vault plan 07): Home.md is gone from the memory root, Filing.md is gone from the vault root, no note links to either, and index.md β the vault's one hand-kept root map β carries the write-authority table exactly once. A link is matched by its text, not by where it resolves: with the map gone, a bare [[Home]] would quietly open personal/Home/β¦/Home.md, and a dangling-link check would read that as green β this gate checks the name instead of the resolution. A note under the operator's own personal/, and a link shown inside a code span, are never findings β there is no other directory-based exemption; a wikilink inside a project's tasks/<slug>/plan.md is checked like any other note (the retired vault _harness/ carried none either, before agentm-vault plan 15 removed the directory). Resolved at runtime, over the live vault. Same two-state shape as the card gates, on its own marker: reports what it finds and exits 0 until the maps data run writes memory/.maps-and-root-notes-complete; enforces from then on. |
scripts/check-root-notes.py |
| check-index-self-description |
index.md describes the vault in the payload's terms (agentm-vault plan 12): the map carries the payload's reading order and its card guide, minus the per-surface paragraph, which is the one part of the payload that is about the reader rather than about the vault. The reading order has to name the three things that never move β standards/, moc-projects.md, and the project's own tracker.md / charter.md; the card guide has to name every field in the card's order and define the words a reader cannot guess (unfiled, dormant, superseded, completed/, agent/archive/). It has to name standards/storage-rules.md as the file that decides where a capture goes β a pointer, never a copy, because the contract is the one gated source of truth for routing and a second copy in the map would be covered by none of its guards. And it must not send a reader to Filing.md, which folded into index.md in plan 07; surviving that fold is why the design chose a pointer. Resolved at runtime over the live vault, with a self-test that runs everywhere: a complete section passes, a hole and a retired pointer each fail. No vault is a named skip, never a silent pass. |
scripts/check-index-self-description.py |
| check-always-load-budget | The always-load tier stays under 40,000 tokens β the operator's number (agentm-vault Β§ the always-load tier). Every session reads this tier whole before it reads anything else, which makes it the one cost paid on every prompt of every session without being asked. The gate reads the packaged set β the shipped templates plus the packaged filing contract, what an install puts on a fresh machine β and reports the live tier beside it without enforcing: a tier that grew because the operator wrote more is theirs, and a budget that makes them count words in user-preferences.md is the wrong pressure. The ceiling is against creep. Measured as injected, not as stored, through the loader's own stripper: the contract's fenced storage-rules block is parsed out of the file by the Go daemon and never reaches a session, so counting it would gate on bytes nobody is charged for and hide real creep behind a number that was already mostly machine block. Self-test runs everywhere: an oversized set fails, and a fenced block is shown not to be counted. The live tier's own over-ceiling warning is a line printed by the brief hook, never by the loader β a large SessionStart output is collapsed unread, so a warning there is a warning nobody sees. |
scripts/check-always-load-budget.py |
| check-measurement-surface | A pass that searches in order to grade searching says so (agentm-vault plan 12). Since the daemon writes the clock for every hit it serves, agentmd search is no longer free: a query is a statement that somebody read the note, and the note is held at day zero for a year because of it. Most callers in this repo are not somebody reading β the retrieval gate runs 64 questions a night against the same gold set, and the scorecards, the probe, the lifecycle eval, the alias pilot and the filing engine's similarity shortlist all query with nobody looking at the result. Unlabelled they would hold roughly three hundred notes young in perpetuity, and the corpus would read as healthy while the axis measured nothing. A measurement caller passes -surface measure; this gate is what keeps the next one from forgetting. A grep over a named list, deliberately: the failure is a missing argument at a call site, which is exactly what a grep can see. Why the default is not measure: a person running agentmd search is reading their memory, and the two failure directions are not symmetric β a real recall uncounted ages a note the operator is using, where a measurement counted keeps a note young, which shows in the morning note and costs nothing but rank. Self-test runs everywhere. |
scripts/check-measurement-surface.py |
| check-calendar-root | The calendar root holds only years and their maps (agentm-vault plan 07): calendar/ is facet notes under year directories, each year's generated map (moc-calendar-YYYY.md) beside its directory, and nothing else. One thing stays allowed at the root permanently β Obsidian's daily-notes setting names calendar/_daily-template.md as its template, so the template lives where the setting names it (agentm-vault plan 15 decided this; plan 10 had left it open) β reported as a note, not a failure. The bare-date day note allowance (YYYY-MM-DD.md) retired with plan 10: the daily-notes format is YYYY/YYYY-MM-DD-diary, so a day note lands in its year, and a bare-date file at the root is a finding like any other stray. A year whose first facet note landed today, before the night has written its map, is a note too. Resolved at runtime, over the live vault. Same two-state shape as check-root-notes, on the same marker: reports until the maps data run writes memory/.maps-and-root-notes-complete; enforces from then on. |
scripts/check-calendar-root.py |
| check-tracker-schema | Every tracker in the projects space has the tracker's one schema (agentm-vault Β§ Projects and tasks), in exactly one of three places: a project's own tracker.md, a task's tasks/<task>/tracker.md, or a moved task's completed/tasks/<task>/tracker.md β where the night moves a closed task's folder two weeks after it closes (task 177). A tracker names the project it sits in, a task's tracker names its task, and a project's own tracker names no task; a note declaring kind: tracker anywhere else β a tracker beside a flat plan pair in a copy of the retired _harness/ among them, since that directory is gone (agentm-vault plan 15) β is reported as misplaced. Every run self-tests against fixtures first β a good tracker in each place, a malformed one, a mismatched one, a misplaced one β so a vault-less machine still exercises the checks. Trackers exist now, since the crickets release writes them, so the gate reads every one in the live vault: on 2026-09-20, 295 trackers and 2 findings, both in files the operator owns, which keep check-all red on this gate until the operator resolves them. No direct CI step of its own β runs via the UNIT_WRAPPED allowlist (test_check_tracker_schema.py subprocess-invokes it), same pattern as check-ci-consistency. |
scripts/check-tracker-schema.py |
| check-memory-root-consistency | The two roots must agree about where memory lives. Two invariants, because one alone missed a real failure: (1) every daemon.spaces value sits at or beneath memory_root; (2) where the Go daemon (daemon.spaces) and the Python stack (plugins.obsidian-vault.spaces, relative to memory_root) both name the same space, they must resolve to the same directory β containment alone isn't agreement. Absent keys are not failures: an unset memory_root defaults to the vault root, so every space is trivially beneath it (the pre-cutover topology), and a space the Python side doesn't name falls through to its own default rather than being compared. One designed exception (filing-v2 part 2b): daemon.spaces["projects"] = "Projects" names the vault-root sibling deliberately outside memory_root, so containment is waived for exactly that value β agreement still holds, checked against the Python side naming the same sibling as ../Projects (explicit or its default). test_memory_root.py covers the base invariants; test_projects_merge_layouts.py pins the sibling exception. |
scripts/check-memory-root-consistency.py |
| check-no-hardcoded-vault-path | No tracked non-test file contains a hardcoded absolute vault path: (A) β¦/Library/CloudStorage/β¦ literals (not shell tilde/variable expansions); (B) the retired pre-V5-3 vault root name as a path component (β¦/Obsidian/AgentMemory). Ensures vault-path hygiene survives refactors β callers must resolve the live path at runtime, not bake a machine-specific or stale-root path into source. |
scripts/check-no-hardcoded-vault-path.py |
| check-no-title-case-roots | No literal names a vault root space by its retired Title Case spelling β the root spaces are lowercase since the root casing (agentm-vault plan 08): agent/, calendar/, personal/, projects/ beside standards/. The operator's disk is case-insensitive, so a stale Agent/memory/... still opens locally, and the deterministic battery alone can't catch it β what breaks is every string comparison on a vault-relative path, and Linux CI, where the same open fails too. Two forms count, comment lines skipped: the path form anywhere (a root name before a slash, or closing a path after one β "../Projects" hits, ~/Projects the operator's home passes) and, in a code file (.py .go .sh .ps1), the bare quoted name ("Projects"). Allowed, and reported as allowed by --inventory: wiki/, CHANGELOG.md, the frozen measurements under scripts/health/results/ and scripts/health/fixtures/ (the gold set included), the September payload fixture, the seven finished migrations under scripts/migrate/ with their tests, and any line carrying the marker root-casing: <why>. The repo-local .harness/ is walked past entirely (_SKIP_DIRS), never reported as an allowed hit; there is no separate _harness/-tree allowance β that vault directory retired under agentm-vault plan 15. --inventory prints every hit with a count instead of failing. Runs as a step of Linux CI's validate job, and in check-all.sh directly after check-no-hardcoded-vault-path. |
scripts/check-no-title-case-roots.py |
| check-no-harness-paths | No tracked code names the retired per-project state directory, _harness/. A project's state root has been the project directory itself since the projects migration (agentm-vault plan 10, 2026-09-16): tasks under tasks/NNN-<slug>/, machine files under desk/, designs under designs/, followups.md and roadmap.md under docs/ (moved off the root by task 176 step 3, 2026-09-25). Plan 15 retired every fallback that still reached for the vault's _harness/, and this gate keeps the word out of the code afterward β a reader that composes _harness/ and tests is_dir() fails soft on a vault where the directory is gone, so nothing breaks and nothing says so, which is why no other deterministic check catches it. A hit is the literal _harness not glued to a longer identifier (resolve_harness_root and test_harness_memory.py are not hits), on any line, comments included β a comment naming _harness/ as a place teaches the next reader a place that no longer exists. Scans the repo's tracked files with a code or text suffix; test files are excluded, since a test may build the old layout on purpose to prove it's refused. Allowed, and reported as allowed by --inventory: wiki/, CHANGELOG.md, scripts/health/results/ and scripts/health/fixtures/ (the gold set among them β the eval translates a moved path at score time, never in the fixture); a line carrying the marker harness-deprecation: <why> naming where the literal sits (an eval remap row's old side, a gate's own list of refused words); and a whole file whose opening 40 lines carry harness-deprecation: file β a finished migration, which records the layout of its day and whose revert compares against what it recorded. The repo-local .harness/ (init.sh, verify.sh, project.json β¦) is a different directory and is never a hit. --inventory prints every hit with a count and exits 0; the day plan 15 task 1 landed it, the inventory read 204 literals to retire across 48 files, with 857 allowed. Enforces now β plan 15 task 1 registered the gate --inventory-only on purpose, so the code could be brought clean incrementally without redding the battery mid-cleanup; plan 15 task 3 dropped --inventory, and the gate reads 0 literals left to retire (930 allowed, across 63 files, as of 2026-09-23 β the number moves as pages are added or edited, but 0-to-retire is the invariant the gate enforces). The doctor's harness-dirs row is the other half of the same guard: this gate keeps the word out of the code, that row catches a directory that came back on disk (see harness/skills/doctor.md). Runs as a step in tests-linux.yml, and in check-all.sh directly after check-no-title-case-roots. |
scripts/check-no-harness-paths.py |
| check-registry-hygiene | No root_path in the live repo registry sits under a temp root. Since the memory-root trims (agentm-vault plan 05) the registry's home is <engine state dir>/repos.json; the vault's _meta/repos.json is read as the legacy copy only while it is the sole one (scripts/repo_registry.py's registry_store()), and this gate's own default resolution follows the same order β $TMPDIR, /tmp, /var/folders, or their realpath forms. A test that builds a fixture repo in a temp directory and registers it leaves an entry whose path died with the test, and every reader of the registry is then handed a repo that does not exist. Three accumulated in the vault's copy by 2026-08-10 (novault-marker, redetect-cli, redetect-demo) because the tests that made them popped $MEMORY_ROOT to simulate "no vault" without redirecting $AGENTM_INSTALL_PREFIX, so vault_path() fell through to the operator's real config. In the vault they kept repos.json modified-but-uncommitted, and while the daemon committed markdown only that held agentmd gate corpus-write shut; that gate reads the vault's git status, and the registry has not been there since plan 05. Since the move, $AGENTM_STATE_DIR is what keeps a registration off the machine. The battery's runner gives every test its own, so a leak now comes from a suite run by hand (python3 scripts/test_<name>.py), and this gate catches it on the next battery run; until 2026-09-13 a hand run of test_project_config.py added five throwaway entries. A test that registers a repo takes an engine state directory of its own with engine_state_isolation.isolate_engine_state(self), or isolate_module(globals()) for a whole module, and no_vault_configured() in scripts/test_project_config.py redirects the state directory along with $MEMORY_ROOT and $AGENTM_INSTALL_PREFIX. This gate is the tripwire for the ones that don't. Graceful-skips when there is no registry to read. |
scripts/check-registry-hygiene.py |
| check-project-yaml | Every live project's projects/<slug>/project.yaml parses to a mapping and carries slug (matching the directory name), title (non-empty), status (one of queued, active, parked, done, dropped), repositories (a list of owner/repo), and code_paths (a list of home-relative ~/β¦ paths), plus the optional board (owner + a positive number), sensitivity (one lowercase word), former_names (a list of owner/repo, for a project that lists exactly one repository; task 186) and bare_issue_floor (a positive whole number, for a project that lists a repository; task 187); any other key is a finding. standards/templates/project.yaml is checked the same way, minus the slug requirement it can't carry. Added by tasks/176-converge-the-vault-layout step 5. Resolves the vault at runtime and skips cleanly (exit 0) when none resolves β CI has no vault. Runs in scan mode as a check-all.sh step directly after check-tracker-schema; tests-linux.yml runs --self-test instead, directly after check-vault-frontmatter's self-test. Reads 0 findings across all 12 live projects as of 2026-09-25. See Vault project.yaml reference. |
scripts/check-project-yaml.py |
| verify-v4 | Kernel-owned auto-orchestration contracts: config seed/parse (A), idle-chain dry-run ordering + bounded execution (E), emit gating (shifted-guard + cooldown) + atomic state write + single-writer invariant (G static) β against a throwaway scratch vault. Linux/Mac only. | scripts/verify-v4.sh |
| verify-orchestration-briefing | PM-half of the orchestration push surface (V5-5 / LC-9): briefing signals (inbox Β· HIGH watchlist Β· incubator Β· staged-adapt) Β· staged-adapt surfaces-and-clears Β· the stale-promotion nudge. 9 checks against a throwaway scratch vault (the idea-ledger signal and its promote-suggest nudge retired in agentm-vault part 13; the script now asserts an old-shape Ideas.md raises neither). Runs as a direct step in tests-linux.yml/tests-mac.yml (previously check-all.sh-only and invisible to CI; testInfra#1). Linux/Mac only. Session-marker and phase-dispatch scenarios are in verify-phases. |
scripts/verify-orchestration-briefing.sh |
| verify-idle-chain | The non-dry-run counterpart to verify-v4's segment E (issue #71, Hardening I #45 follow-up). Runs the idle chain and a post-release dispatch for real against a scratch vault and scratch transcript root, asserting the mutations rather than the plan: the mined preference that lands, the seen-state that advances, the candidates that stage, the cooldown that is recorded and then honored, the skill-pointer entry the dispatch writes. Hermetic β no network (comments-only discovery whitelist, seeded cadence state, no GitHub links in the adapt fixture), no real vault, no real transcripts. VERIFY_IDLE_CHAIN_FAULT=dry-run re-runs segment A with --dry-run so every mutation assertion fails, proving the gate catches a chain that plans and never does. Manual/local only; CI runs the plain invocation. Linux only. |
scripts/verify-idle-chain.sh |
| verify-hook-resolution | Regression guard for the hook-config dual-key vault_path read (R1.2 / agentmEngine#0): builds a scratch HOME with a .agentm-config.json carrying ONLY the plugin-namespaced key (plugins.obsidian-vault.vault_path) and a scratch vault, then runs all four memory hooks (session-start, prompt-submit, reflect-stop, reflect-idle) against it and asserts each one actually resolved the vault. VERIFY_HOOK_RESOLUTION_FAULT=1 strips the vault key entirely so the same assertions fail loudly, proving the fault-injection mode has teeth (mirrors the VERIFY_MEMORY_FAULT convention in verify-memory-roundtrip.sh). Runs as a direct step in tests-linux.yml/tests-mac.yml. Linux/Mac only. |
scripts/verify-hook-resolution.sh |
| verify-state-routing | Regression guard for the backend Γ project-mode routing matrix (R1.3 / agentmEngine#1) plus the never-demote invariant, proven against the real resolve-active-plan and append-progress CLIs β a named plan is resolved the way /plan resolves it, its progress log seeded where the resolver points, and the kernel's append-progress write proven per mode: (A) storage.backend resolving to vault routes the plan to a task under the project's tasks/ directory in the vault, never the repo-local .harness/ (agentm-vault plan 15 dropped the vault-side _harness/ this used to route to); a bare call names no plan (exit 4); (B) .harness/.project-mode=local opts out even with a vault configured; (C) no backend configured falls back to device-local .harness/; (D, unconditional) storage.backend=vault explicitly configured with a broken/missing vault directory makes both CLIs exit non-zero and never silently write to the device-local fallback β the never-demote guard from Storage seam Β§ the fail-loud guard. VERIFY_STATE_ROUTING_FAULT=1 reproduces the historical pre-fix bare-except Exception shape inline (a local shadow, never patching production code) and asserts it would have demoted, proving the fixture is faithful to the original bug. Writing this check surfaced a real module-identity bug in harness_memory.py, fixed in the same change β see Storage seam Β§ the fail-loud guard for the mechanism. Runs as a direct step in tests-linux.yml/tests-mac.yml. Linux/Mac only. |
scripts/verify-state-routing.sh |
| verify-reflection | Regression guard for two independent invariants of the reflection pipeline (R1.5 / agentmExperience#2, the R0.3 source-filter fix; the fixture literals below picked up a durability cue in the miner-provenance follow-up, PR #546 / 2026-09-04): (1) classification, mode-independent β calls reflect.mine_transcript() directly; a genuine operator preference statement carrying a durability cue ("From now on I prefer kebab-case slugs...") classifies HIGH β PR #546 made a durability cue (always, never, from now on, going forward, in general, every time, whenever, by default, as a rule) a requirement for HIGH, so the same sentence without one classifies MEDIUM β while a junk corpus of bare always/never phrasing ("the deploy job always crashes...") never classifies HIGH (R0.3 demoted the bare always/never pattern out of the HIGH lane, since it fires on ordinary discussion just as readily as on operator directives, a demotion the durability-cue rule leaves in place); (2) source filtering β runs the real reflect.py corpus CLI across all three --route-mode values (auto/silent/interactive); a transcript under a subagents/ path, carrying the same still-HIGH, durability-cued preference pattern repeated 3x, never contributes anything to the vault in any mode, because _discover_transcripts excludes subagents/ paths before mine_transcript ever runs on them. VERIFY_REFLECTION_FAULT=1 calls mine_transcript() directly on the machine-source fixture (bypassing _discover_transcripts entirely) and asserts it WOULD classify HIGH, proving the fixture is a faithful trigger for the historical junk-slug pollution bug the filter exists to prevent β manual/local-only (CI runs only the plain invocation). Runs as a direct step in tests-linux.yml/tests-mac.yml. Linux/Mac only. |
scripts/verify-reflection.sh |
| health-score-determinism | The health-scorecard aggregator (scripts/health/health_score.py, stdlib-only) reads JSONL check records from stdin or --path and groups them by axis β one of eight locked family names, each with a fixed weight: memory persist+recall 25, plan-adherence+drift 15, verification honesty 15, capability function 15, memory freshness+experience 10, efficiency 10, docs+voice health 5, safety/recoverability 5. Each axis/family score is 100 Γ sum(weight for passing checks)/sum(weight), excluding {"pass": null, "dark": true} dark (designed-not-built) records from both numerator and denominator; the Health Index is the weighted mean across only families with at least one non-dark record, so an untested family doesn't drag the index down. --check-determinism runs the aggregator twice against the same fixture input and asserts byte-identical markdown output. Task 1 of 5 in the R1 dashboard plan (R1.8). Task 2 wired real JSONL emission into all nine verify-* suites (--jsonl-out/$HEALTH_JSONL_OUT, additive-only β see scripts/health/README.md for the pipeline and the per-suite axis mapping); this row still covers only the aggregator's own determinism, not suite-level JSONL correctness. |
scripts/health/health_score.py |
| validate-audit-coverage | Task 3 of the R1 dashboard plan (R1.8): proves the regression net actually catches what the mythos-readiness audit caught by hand. The audit ledger names 11 verified blockers; this gate covers 4 β the ones with real code presence in this repo (agentmEngine#0, agentmEngine#1, agTrack#0, voice#0); agentmExperience#0 (a dead vec-index drain) was a fifth until the vector stack was removed and its subsystem stopped existing β and explicitly reports the other 6 as out of reach for an agentm-repo verify script rather than silently omitting them. Of those 6, 3 (cricketsPluginsA#0, cricketsPluginsA#1, cricketsPluginsB#0 β the find_capability fallback chain, the finalize_unit PII guard fail-open, and the pricing re-pin) now have dashboard visibility from the other side of the seam: crickets' scripts/health/run-crickets-fast-tier.sh (added PLAN-r2-ledger-and-dist task 7) emits a capability function check record per blocker via the new shared crickets/scripts/health/jsonl_emit.py helper, consumable cross-repo the same way this script's own records are. The residual genuinely-out-of-scope count is 3 β roadmapMaster#0, wikiAgentm#0, wikiCrickets#0, all documentation/content staleness with no fault-injection concept. Operator-confirmed scope narrowing, 2026-07-03; cross-repo dashboard-visibility update, 2026-07-05 β see the script's own header comment for the full per-blocker breakdown and detection mechanism. |
scripts/health/validate-audit-coverage.sh |
| verify-dreaming | End-to-end regression guard for the Python half of the night and the health-scorecard's live signal for the memory freshness+experience axis's dreaming row: a twin found, no note changed, the pair on the needs-review map. It runs the real dream.py CLI against a scratch vault seeded with a near-duplicate pair. A: the run reports one possible twin, and the digest lists the pair under "For you to judge". B: both notes are byte-identical after the run. C: the needs-review map carries a ## Possible twins (1) section naming both notes, and the run leaves no staging directory and no _dream/ insight. F: templates/jobs/dream.yaml parses through the runner's manifest schema, starts inside the 02:00-06:00 window at order 3, and stays dry_run: true. The insight check, the confirm-and-revert round trip and the expired-proposal refusal retired with the stages they exercised (agentm-vault plan 04). Unit coverage of each stage lives in test_dream.py, test_needs_review.py and test_dream_job.py; this script proves the wiring. |
scripts/verify-dreaming.sh |
| check-dreaming-parity | The dreaming binary (agentmdream, daemon/internal/dreaming) reproduces the Python pass it replaced. scripts/fixtures/dreaming-parity/expected.json was recorded from the Python producers, clock pinned, before they retired with the takeover (2026-09-05) β the recording is the contract now, and it cannot be re-recorded, so a changed decision is a deliberate edit to it, made on purpose. Runs the Go tests (TestParityWithTheRecordedPythonPass, TestCalendarReviewsMatchTheRecordedPythonText β the calendar reviews compared byte for byte) as a direct step in tests-linux.yml and in the local battery. Needs Go. Promote's own half is narrowed away (agentm-vault plan 04, task 4): it no longer writes the crystallized digest the recording captured, so the recording's promote key sits unread β only the lifecycle and copies halves still check byte for byte. The card backfill (agentm-vault plan 06) made a second deliberate edit to the recording, on the same principle: the copies half's two after fixtures now carry slug: last, after superseded_by:, instead of just after status:, because the copies job now puts its patched note back in the card's order and slug is a machine field β the only change, made on purpose. |
scripts/check-dreaming-parity.sh |
| verify-declare-a-type | The declare-a-type worked path holds across the seam: contract edit, coverage falls, the batch queue offers it, coverage climbs, against the real binary. The Go end-to-end (daemon/cmd/agentmd/declare_a_type_test.go) covers the mechanism; this script covers the seam between the built agentmd, the shipped Python and a vault on disk, with nothing stubbed. A changed flag name, JSON field or exit code breaks the harness without breaking a Go test, and this is where it shows. Seven checks over a six-card corpus. A: a corpus enriched under one filing contract reads as fully covered. B: declaring a type changes the contract hash. C: coverage falls to zero and every item's reason is stale. D: the reason text names the filing contract. E: the enrichment batch's own queue, read through agentmd enrich --dry-run, offers every stale card, and none reads as unchanged. F: re-enriching under the new contract brings coverage back. G: the batch then reads every re-enriched card as unchanged at this pass. E and G were narrowed in agentm-vault plan 04; they used to drive the unfiled drain and the entity-rollup stage, both retired. The script builds agentmd from source, so it needs Go and skips without it. Runs as a direct step in tests-linux.yml/tests-mac.yml and in the local battery. |
scripts/verify-declare-a-type.sh |
| check-job-templates | Every shipped runner job template under templates/jobs/ loads through the runner's own manifest parser (scripts/runner/manifest.py) β the same parser scripts/agentm-runner.sh uses at register time. Found by the bug it now guards against (#557): templates/jobs/dreaming.yaml's command: opened with a quoted path and kept going past the closing quote, a YAML parse error, while retrieval-gate-nightly.yaml and online-recall-nightly.yaml both carried tier: T1, which the runner refuses outright β at the time, the runner loaded every manifest or none, so one bad template stopped every scheduled job with nothing but a launchd-log traceback to show for it. Fixed since (filing-v2 remainders task 1): a refused manifest no longer stops the ones that loaded, and this gate still keeps a bad template from ever being one of them. Runs via the auto-discovered unit tests step; no separate CI step of its own. |
scripts/test_job_templates.py |
| verify-auto-org-meters | End-to-end regression guard for auto-organization's two vault-health meters (PLAN-auto-org-dedup-and-lint.md task 8) and the health-scorecard's live signal for the memory freshness+experience axis's meters row β drives the real dream.run_dream() against a scratch vault seeded with a linked pair, an unlinked note, a shelved artifact, and an archived memory, asserting the organic-connectivity ratio and the three-state (live/shelved/archived) browse-surface count both land in corpus_stats and render in the digest, and that the archived note's content stays readable on disk β the plan's own acceptance test, "browsing agentm's memory shows live, current notes, and aged material sits in the archive, still there on request." Unit-level coverage of each meter's own logic lives in test_dream.py's ConnectivityMeterTests/BrowseSurfaceCountsTests; this script proves the two meters' wiring through a real cycle, not every branch. |
scripts/verify-auto-org-meters.sh |
| verify-phases | A full phase lifecycle (/setup β /plan β /work β /release) drives its deterministic seams β state read/write, progress.md appends, features.json updates, post-phase dispatch plumbing β end-to-end on a throwaway fixture project, run twice: once vault-resident, once repo-local. Also covers session-marker scenarios (no-session / single-marker / ambiguous concurrency-safe) and post-release discover-skills chain (V5-5 / LC-9). Its crystallization section proves the staging step stays retired (agentm-vault plan 04): a real, non-dry-run post-work and post-release dispatch with a live session marker both return a status, report no crystallization key, and leave no crystallize-staging/ directory. Linux/Mac only. |
scripts/verify-phases.sh |
| run-ablation-baseline | For each of the four R3.1a subsystems (vectors, reflection, gates, evidence-tracker), runs its verify-* script twice β once on, once with its ABLATE_*=1 off-switch β scores each run's check records through health_score.py's own axis-scoring path, and emits one ablation record per subsystem ({subsystem, axis, score_on, score_off, uplift}). Deterministic, fixture-driven, no live model calls. The uplift magnitude is advisory reading only β the gate's own exit code is 0 unless a subsystem's on/off pair can't be scored at all (a setup failure, not a low uplift number). |
scripts/health/run-ablation-baseline.sh |
| verify-memory-roundtrip | The memory engine round-trips on a throwaway fixture vault: stub-mode embed (deterministic hash vector, no network/model) β save β recall query surfaces it by content β reflect a synthetic transcript β vault_lint clean. A VERIFY_MEMORY_FAULT=drop-save injection drives the negative path. The index-build and nearest-neighbor sub-checks went with the vector stack (week-1 experiment); embed is still round-tripped because notes_link_discovery --embeddings still uses it. Hermetic, Linux/Mac only. |
scripts/verify-memory-roundtrip.sh |
| dogfood-workflows | Every workflow the harness ships as a template under templates/.github/workflows/ is active at the repo root, byte-identical to the template. Mirrored locally by check-vendored-parity's workflow mode (above), so a one-sided edit is caught in check-all.sh before the push, not as a red Linux run after it. |
Inline job in tests-linux.yml
|
| check-ci-consistency | The both-places rule (testInfra#3): every gate "<name>" <command...> line in check-all.sh also runs in CI β either as a direct tests-linux.yml step, or via a documented UNIT_WRAPPED allowlist (see scripts/test_ci_consistency.py for the current entries β the count itself isn't restated here, since a hardcoded number just drifts) whose wrapper test_*.py genuinely subprocess-invokes the gate by name. Also checks gate-flag parity (--strict/--all) between check-all.sh and tests-linux.yml, and that ci-all.yml's WORKFLOWS list points at real workflow files. Runs via the auto-discovered unit tests step, same pattern as check-vendored-parity's workflow mode β no separate CI step of its own. |
scripts/test_ci_consistency.py |
This script checks the V4 #23 kernel-owned auto-orchestration contracts. It runs the real scripts via their CLIs against a throwaway scratch vault and asserts deterministic outputs. It acts as the integration complement to the per-function unit suite. We trimmed it to 85 lines at V5-5 / LC-9 when we extracted the PM-half and Developer-half.
| Property | Detail |
|---|---|
| Isolation | A mktemp -d scratch vault β never reads or writes a real vault. |
| No side effects | No network, no transcript mining, no sub-agent dispatch; self-cleans via a trap. |
| Coverage |
Segment A: config seed/parse. Segment E: idle-chain dry-run (ordering + bounded --max-batches/--limit). Segment G: emit gating (shifted-guard + cooldown) Β· atomic state write Β· static single-writer check: greps orchestration_*.py for ^def save_state and asserts 0 matches β making the V5-5 LC-2 single-writer invariant a gate-checked property. |
| Out of scope | Briefing signals, staged-adapt, nudge triggers, phase-dispatch, and session-marker scenarios β those moved to verify-orchestration-briefing.sh (PM-half) and verify-phases.sh (Developer-half). |
| Extend it | Add a check_* per new kernel-owned contract (drive the real script against $SV; assert with assert_contains/assert_equals/assert_absent), keeping each check scratch-isolated. |
This script checks the PM-owned orchestration signals extracted from verify-v4.sh at V5-5 / LC-9. It will travel to the crickets PM-trigger plan when that ships. It runs agentm-side until then.
| Property | Detail |
|---|---|
| Isolation | A mktemp -d scratch vault β never reads or writes a real vault. |
| No side effects | No network, no transcript mining, no sub-agent dispatch; self-cleans via a trap. |
| Coverage | Every briefing signal: inbox Β· HIGH watchlist Β· incubator Β· staged-adapt Β· staged-adapt surfaces-and-clears Β· the stale-promotion (watchlist-authoring) nudge Β· phase-dispatch plans + session-marker resolution (no-session / single-marker / ambiguous concurrency-safe). 9 checks. The idea-ledger signal and its promote-suggest nudge retired in agentm-vault part 13 β two assertions now confirm an old-shape Ideas.md raises neither. |
| Out of scope | Kernel contracts (config seed, idle chain, emit gating) β those remain in verify-v4.sh. |
| Extend it | Add a check_* per new PM-half signal, keeping each check scratch-isolated. |
verify-v4.sh segment E only asserted the idle chain's dry-run plan. That tests planning order and leaves every step free to no-op silently β the V4 #39 shape. This script closes the gap (issue #71): it runs the same chain without --dry-run and asserts what changed on disk.
Every assertion pins to observed reality β a file that exists, a literal from the fixture, a count from find. None recompute an expectation with the implementation's own formula, because a check that derives its expectation from the code under test only proves the two agree.
| Property | Detail |
|---|---|
| Isolation | One mktemp -d holds a scratch vault and a scratch transcript root, removed via a trap. |
| No network | The discovery whitelist is seeded comments-only and the cadence state is seeded fresh, so discover_skills.py --cadence-check returns before it fetches. Both are seeded, so a machine whose clock makes cadence miss still stays offline. The adapt fixture carries no GitHub links, keeping Pass-1 enrichment away from api.github.com. |
| No real transcripts |
MEMORY_TRANSCRIPT_ROOT points at the fixture tree. The chain invokes reflect.py corpus without --projects-root, so that env var is the only thing standing between this gate and your real ~/.claude/projects. Segment A asserts the seen-state names exactly the fixture session, which is what catches that plumbing breaking. |
| Coverage |
A: the real chain β the mined preference lands in the vault, the seen-state advances, both candidates stage under the names the fixture headings imply, the reported staged_candidates matches the files on disk, last_fire is recorded. B: a second real run is a cooldown no-op that stages nothing new. C: enable_idle_chain = false mines and stages nothing. D: a discover step that did not throttle is never labelled throttled. E: a real post-release dispatch writes the skill-pointer entry. |
| Negative check |
VERIFY_IDLE_CHAIN_FAULT=dry-run runs segment A with --dry-run β the regression this gate exists to catch β and every segment-A mutation assertion fails. Manual/local only; CI runs the plain invocation. |
| Found in the writing | Segment D guards a bug this gate surfaced. The discover-skills label grepped stdout for cadence and skip, which matched the "cadence_skipped" key, so every run reported throttled β including one that fetched all four network sources. The label now reads the flag. |
| Extend it | Add a segment per new chain behavior. Assert the mutation, never the plan. |
gh run list --workflow "[T] Linux Tests" --limit 3
gh run list --workflow "[T] Mac Tests" --limit 3
gh run list --workflow "[T] Windows Tests" --limit 3
gh run view <run-id> --log-failed # drill into the failing stepA red run on Windows but a green run on POSIX indicates a path-separator or pwsh-host assumption regression. A red run across all operating systems indicates a canonical-spec or adapter-parity drift. Try running bash scripts/check-parity.sh locally to verify.
Not every red run is a failure. A run can be marked failure having dispatched
no work at all, and it reads exactly like a broken build until you look closer.
The signature. The run completed within a few seconds of being created, and
its jobs sit at status: queued with an empty steps list β GitHub accepted the
run and never assigned it a runner. A genuine failure has a job with
conclusion: failure and a named failing step.
# created and updated seconds apart => suspect an orphan, not a failure
gh api repos/<owner>/<repo>/actions/runs/<run-id> \
--jq '{created: .created_at, updated: .updated_at, attempt: .run_attempt}'
# zero failed jobs on a "failed" run confirms it
gh api repos/<owner>/<repo>/actions/runs/<run-id>/jobs \
--jq '[.jobs[] | select(.conclusion=="failure")] | length'Why it blocks the PR. Those undispatched jobs stay pinned to the pull
request as pending forever. They are not slow; nothing will ever report them.
The PR sits BLOCKED on checks that cannot complete, and ci-all fails
honestly, because from its side a run that never ran and a run that failed look
identical.
Recovery, in order.
-
gh run rerun <run-id>β but verify it took. It can no-op silently; the run should move torun_attempt: 2. A run still reportingattempt=1after a rerun did not restart. - If the rerun does not take, close and reopen the pull request. That fires
fresh
pull_requestevents and produces new run IDs, which is the reliable fix. Note it also clears auto-merge, so re-arm withgh pr merge --autoafterwards. - Re-run
ci-alllast, never alongside the OS workflows. It pollstests-linux/tests-mac/tests-windowsand requiressuccessfrom each, so starting it while they are queued makes it fail waiting on its own inputs.
Reading the aftermath. GitHub keeps orphaned runs in the commit's history,
so the PR page shows old red beside new green. Trust gh pr checks and
mergeStateStatus over the run list β and compare run IDs: the fresh set shares
a higher ID prefix than the abandoned one.
Why there is no concurrency: block anywhere, and why adding one would not help
here: the orphaned runs are the current head's, and concurrency only
cancels superseded runs. It would not have prevented any of this. It would also
turn every routine force-push into an aggregate failure, since a cancelled run
is not success and that is the only value the poller accepts. The push:
trigger is filtered to branches: [main], so PR pushes do not double-fire
either β there is no duplication to collapse.
The [H] Health Nightly workflow (.github/workflows/health-nightly.yml, R1.8 Task 4) runs daily at 06:00 UTC and on manual workflow_dispatch. It layers heavier checks on top of the fast tier this page documents. These heavier checks are too slow or dependency-heavy to run on every push. They include real-embedding recall (no stub mode), a live MCP daemon round-trip over real HTTP, a report of every VERIFY_*_FAULT=1 mode's outcome, and a cold-install dogfood pass. The job renders the scorecard and uploads it (HTML and the raw JSONL records) as a build artifact named health-scorecard-ubuntu with a 14-day retention. It never commits or pushes. The V8 proving Lane S on 2026-07-13 retired the branch/PR/workflow_dispatch/--admin merge machinery this job used to run. The job acts purely as a clean-runner regression signal.
R1.8 Task 5 added a health-nightly-macos job (macos-latest). This job runs the same heavy tier as a conformance leg. It uploads its own JSONL output as a build artifact named health-heavy-tier-macos with a 14-day retention. Task 5 also folds the static scripts/health/dark-checks.jsonl registry (5 designed-not-built capabilities) into the render pipeline. The scorecard carries a "Dark checks" section that lists them separately from the scored families.
You do not read this CI artifact day to day. A local health-pass runner job (templates/jobs/health-pass.yaml, a daily agentm-runner.sh tick) appends to the vault-resolved history ledger and renders ~/.cache/agentm/telemetry/scorecard.html. The /console and the morning brief link to this local file. See Health scorecard for details on that half.
AA4 (2026-07-08) β the β« Hardening-II cold-install acceptance, per the B3 ruling in ROADMAP-TAIL-ADJUDICATIONS.md. We added two more checks to the cold-install dogfood step rather than creating a separate job. (5) dashboard honest-dark on a bare install β an observability-console render over a rollup built from zero telemetry events, and a Health-Scorecard render over only the static dark-checks registry (zero live records). Neither check may fabricate a number when nothing has actually run yet. (6) fan-out/budget gates fail CLOSED with no config β a scratch .harness/ with no budget.yaml must still throttle a fleet that reports large spend. It must not skip the ceiling check outright. As built today, check 5's observability-console half passes. Its Health-Scorecard half and check 6 both FAIL. The render_markdown function shows a fabricated Health Index: 0.0/100 π’ headline on zero live records. The runner/cycle.py _read_daily_ceiling function returns None on a missing budget.yaml. This disables the ceiling gate rather than applying a safe default. We filed both as queued bugfix plans instead of fixing them inline. An acceptance pass reports findings; it doesn't fix them.
This workflow never blocks a merge. The fast tier above (tests-linux.yml/tests-mac.yml) serves as the only merge gate. The nightly run can go red without stopping development.
One command runs the deterministic battery. It executes unit tests, every check-* gate, the integration checks (verify-v4, verify-orchestration-briefing, verify-idle-chain, verify-hook-resolution, verify-state-routing, verify-reflection, verify-dreaming, verify-declare-a-type, verify-auto-org-meters, verify-phases, verify-memory-roundtrip), and run-ablation-baseline. It prints a PASS/FAIL table and exits non-zero on any failure. The scripts/check-all.sh script acts as the living source of truth for the exact gate count. A restated number here drifts as gates merge or land. Count the gate "<name>" ... lines in the script if you need the current figure:
bash scripts/check-all.shThe script deliberately omits the heavier smoke-install and gitleaks checks. CI runs those slower external tools on every push. Run them directly if you need them: bash scripts/smoke-install-bash.sh (POSIX) or pwsh -NoProfile -File scripts/smoke-install-pwsh.ps1 (Windows). The check-all.sh script acts as the maintained source of truth for the local battery. Add a gate line to it as the project grows.
scripts/probe-round-trip.sh reads the memory system's end-to-end claim: save a
fact, come back in a fresh process, ask sideways, get it back. It captures
through the real agentmd, optionally runs one enrichment call, and asks three
questions that share no content word with the note, against twelve distractors,
with a control question that must not return the target.
It is not in check-all.sh and should not be added. It needs a running embedder,
and --enrich spends a model call. A gate with those dependencies is a gate
somebody turns off, and the battery has to stay something you can run on any
machine at any time.
Three properties are worth knowing before you read a result from it. The dense
arm is required β fusion is lexical only, asking sideways is the case the dense
arm exists for, and a run without an embedder exits 2 (cannot run) rather
than passing. Sideways is checked rather than asserted: a question reusing the
note's own words is refused, because that proves the index can find a string, not
that a memory can be recalled. And the rank the target achieves is reported but
not gated, since ranking belongs to the retrieval track rather than to this
check.
bash scripts/probe-round-trip.sh --embedder-url http://127.0.0.1:8901 --embed-model embeddinggemma-300M-Q8_0Add --enrich for the whole arc. Exit codes: 0 every bar met, 1 a bar
missed, 2 could not run.
-
Releasing Conventions β CI must be green before you invoke
ship-release. - How to refresh an installed harness β what a re-run touches vs. leaves alone.
-
Vault write protocol β the protocol the
check-vendored-paritygate'svault-lockmode keeps byte-identical across its two copies. - Foundations HLD β the installer-boundary rule the gates enforce.