Skip to main content

SPEC-096 v0.2 — Wrap-Time Pre-Flight Check for Uncommitted Ratified Artifacts

Status: draft v0.2 — Texi architecture review approved_with_required_revisions on v0.1 (then SPEC-095 — renumbered to SPEC-096 due to collision with Porsche’s machine_id contract). All eight required revisions and three nits applied as v0.2. Author: Donna Reviewer (architecture): Texi Reviewer (governance): Candi Origin: Two ratify-vs-commit gap recurrences on 2026-05-08 (postmortems 2bc9bda2 and 07e9345e).

Summary

Prevent the recurring class of gap where a Prism entity is ratified in TriGraph (spec status=approved, ADR committed, fragment published, nav added) but the disk artifacts that the ratification implies are left uncommitted on local working trees. Peer machines pull stale state on git pull, even though Prism reports the artifact as live. Mechanism: a pre-flight check at prism_checkpoint and prism_wrap time that warns or refuses to close the unit-of-work when the working tree contains uncommitted modifications under a watched path AND the session shows publication/ratification language referencing that path or the artifact ID it implements. Operationalizes Element 4 (“Wrap-time handoff gate”) of method.wall-break.persistence for the specific case of artifact-on-disk vs artifact-in-Prism drift.

Background — observed gap

Time (UTC)ArtifactRatified atDisk landed atGap
2026-05-08T03:02SPEC-094 v0.3 (BIOS auto-memory rule)spec entity status=approvedPR #216 merged 04:19~75 min, peer machines stale
2026-05-08T14:21method.wall-break.persistence v0.2docs.json nav added by CandiPR #222 merged 14:35~14 min, peer caught nav-without-file via Desiree
Both occurrences share the same shape: author edits, author or peer marks ratified in Prism, author wraps without committing, peer or downstream session fetches main and finds the gap.

Watched paths (v0.2 scope, per Texi nit + answer)

The check fires when a working-tree-modified, staged, untracked, renamed, or deleted file matches any of these path families. Per Texi guidance, enforcement priority is split into two tiers: Tier 1 — high-risk paths (eligible for enforcement at v0.3):
  • CLAUDE.md, AGENTS.md, templates/CLAUDE.md, templates/AGENTS.md (BIOS replicas + SORs)
  • docs/method-fragments/*.md, docs/method-fragments/*.mdx (open pattern, not only method.*)
  • docs/specs/spec-*.md, docs/specs/spec-*.mdx
  • docs/adrs/adr-*.md, docs/adrs/adr-*.mdx (corrected from v0.1 docs/decisions)
Tier 2 — advisory-only:
  • docs/case-studies/*.mdx
  • docs/docs.json when the diff adds a navigation entry pointing into a Tier 1 path (see §docs.json detection)
Deferred (with explicit risk callout):
  • templates/PRISM.md — risk acknowledged. PRISM.md sync is already gated by ADR-16 pre-flight; this SPEC defers to that path until v0.3.
  • SPEC-080 governance / method-domain paths.

Trigger semantics — clarified

The pre-flight fires when ALL of:
  • (a) prism_wrap or prism_checkpoint is being requested AND
  • (b) at least one watched path is dirty in the working tree (any of: staged, unstaged-modified, untracked, renamed, deleted) AND
  • (c) the session shows publication/ratification language referencing the watched artifact: at least one of —
    • a session_delta / journal entry / postmortem / signal payload from this session_id containing the literal path, OR
    • a session-emitted reference to a spec_id (e.g. SPEC-096) or fragment_id (e.g. method.wall-break.persistence) that matches the dirty file’s identifier, OR
    • the attempted wrap/checkpoint payload (summary, decisions, next_actions, tags) containing publish-language tokens — publish, ratified, approved, merged, nav added, landed, shipped — adjacent to a watched path or watched artifact ID.
Per Texi: condition (c) does not require proving formal ratification status against TriGraph. The detector matches intent-language in session artifacts; this captures the failure mode at close time.

Behavior — clarified by mode

A new env flag (per Texi nit, replacing v0.1’s boolean):
PRISM_WRAP_PREFLIGHT_MODE = off | advisory | enforce
Default: advisory. Existing boolean PRISM_WRAP_PREFLIGHT_ENFORCED is removed in favor of this trinary.

Mode: off

The check does not run. Verbs proceed as today. Reserved for emergency disable; should not be the default for any tier or environment.

Mode: advisory (default v0.2)

The verb proceeds (delta is written, registration is unaffected) and returns a warnings: [...] list naming the uncommitted paths and the matched references. Matches the existing storeSessionDelta warning pattern. Response shape:
{
  "ok": true,
  "registration_status": "active",
  "delta": { ... },
  "warnings": [
    {
      "kind": "uncommitted_ratified_artifact",
      "uncommitted_paths": ["docs/specs/spec-096-v0-2-wrap-pre-flight-uncommitted-ratified-artifacts.md"],
      "matched_references": [{"path": "docs/specs/spec-096-v0-2-...", "evidence_kind": "next_actions_publish_token", "evidence_excerpt": "publish via Candi"}],
      "branch": "donna/wrap-preflight-v0-2",
      "ahead_by": 0,
      "behind_by": 0,
      "remediation": "git add <paths> && git commit && git push, or pass force=true with reason."
    }
  ]
}

Mode: enforce (v0.3-eligible)

For Tier 1 paths only, the verb refuses:
{
  "ok": false,
  "error": "uncommitted_ratified_artifact",
  "stage": "wrap_preflight",
  "uncommitted_paths": [ ... ],
  "matched_references": [ ... ],
  "remediation": "...",
  "registration_status": "active"
}
Critical lifecycle invariant (per Texi revision 1): the preflight runs before deregistration / heartbeat shutdown / coordination stream stop / signal subscriber close. If preflight refuses, the session remains fully active: registration, heartbeat, signal subscription continue. The agent sees the error and either commits + retries the wrap, or passes force=true with reason. Tier 2 paths (case-studies, docs.json nav-only) stay advisory even in enforce mode at v0.3.

Checkpoint semantics (per Texi revision 2)

prism_checkpoint operates identically in advisory and enforce modes — same response shape — but never deregisters. Checkpoint refusal in enforce mode means the session keeps going; the agent is expected to commit and retry the checkpoint. This is consistent with checkpoint’s “I’m saving my game” semantics: if save fails because state is incoherent, you don’t quit the game.

Force-override (per Texi revision 1, audited)

The agent or operator may pass force=true with a non-empty force_reason string. The verb proceeds, AND records a wrap_preflight_force audit event in TriGraph attributable to session_id. Audit event fields:
  • session_id, agent_identity, force_reason (string, ≥10 chars)
  • uncommitted_paths, matched_references (as collected at force time)
  • wrap_or_checkpoint (which verb was forced)
Audit events become queryable via prism_runtime_diagnostics and feed the v0.3 promotion telemetry decision.

Backend (per Texi revision 4 & 6)

New helper in backend/app/services/wrap_preflight.py:
def detect_uncommitted_ratified_artifacts(
    *,
    session_id: UUID,
    project_id: UUID,
    git_state: GitState,                 # parsed from shim
    attempted_wrap_payload: dict | None, # summary/decisions/next_actions/tags
    mode: PreflightMode,                 # off | advisory | enforce
) -> PreflightOutcome: ...
GitState has fields:
  • dirty_paths: list[DirtyPathEntry] (path, status_code, repo-relative POSIX)
  • branch: str | None
  • head_sha: str | None
  • ahead_by: int | None, behind_by: int | None (None if not cheaply available)
  • git_root: str | None (None if project_dir not in a repo)
  • docs_json_diff: str | None (bounded ≤16KB; populated only if docs/docs.json is dirty)
Backend is responsible for: matching dirty_paths against watched-path families; matching attempted_wrap_payload + recent session artifacts for evidence; emitting warnings or refusal; recording force audit events. detect_uncommitted_ratified_artifacts is called from the prism_wrap and prism_checkpoint handlers immediately after memory-hygiene trigger computation, before any deregistration or stream-shutdown logic.

MCP-node shim changes (per Texi revision 4 & 6)

mcp-node/src/verbs/lifecycle.ts prism_checkpoint and prism_wrap:
  1. Resolve project_dir from cached project context.
  2. Run git status --porcelain=v1 -z with cwd: project_dir, shell: false. Parse \0-separated entries; capture status_code (X+Y from porcelain v1), original path, and rename target if applicable. Normalize all paths to repo-relative POSIX (forward slashes).
  3. Run git rev-parse --abbrev-ref HEAD and git rev-parse HEAD for branch / head_sha. Run git rev-list --left-right --count @{upstream}...HEAD for ahead/behind; on error (e.g. detached HEAD or no upstream), set both to null without failing.
  4. If docs/docs.json appears in dirty_paths, run git diff -- docs/docs.json capped at 16KB and pass as docs_json_diff. Backend parses the diff for navigation-entry additions referencing watched-path families.
  5. Forward the assembled git_state plus the attempted wrap/checkpoint payload to the backend call.
  6. Surface the backend’s response (warnings or refusal) to the model with the remediation hint.
If git is missing or project_dir is not in a repo: shim sends git_state.git_root = null; backend skips the check and returns a preflight_skipped advisory (so calls don’t silently degrade without telemetry).

Phasing

  • v0.1 (renumbered+retired): initial proposal, blocked by Texi review.
  • v0.2 (this SPEC): all required revisions + nits applied. Default mode = advisory. Watched paths split into Tier 1 / Tier 2. Lifecycle ordering invariant explicit. Force-override audited.
  • v0.3: flip default mode to enforce for Tier 1 paths after — (a) Phase 0 advisory telemetry observation period of at least 7 days of active multi-agent sessions, AND (b) Texi + Candi review of telemetry, AND (c) MCP-node + backend test coverage in place. No automatic flip (per Texi nit 3).
  • v0.4 (open): add templates/PRISM.md and SPEC-080 governance paths to Tier 1.

Tests (per Texi revision 8)

MCP-node unit tests in mcp-node/tests/wrap_preflight_shim.test.ts:
  • Porcelain v1 -z parsing: modified, untracked, staged, renamed, deleted, mixed
  • Path normalization (Windows-style → POSIX, nested directories, with spaces)
  • ahead/behind null cases (detached HEAD, no upstream, unreachable remote)
  • docs.json diff capture (≤16KB cap, JSON-valid diff, malformed JSON)
  • Project_dir not-in-repo graceful degradation
Backend unit tests in backend/tests/test_wrap_preflight.py:
  • Path-family matching: BIOS / fragment / spec / ADR (both .md and .mdx) / case-study / docs.json nav-add
  • Reference matching: exact path, spec_id, fragment_id, publish-language tokens in summary/decisions/next_actions/tags
  • Mode behavior: off skips entirely, advisory returns warnings + ok=true, enforce on Tier 1 returns ok=false, enforce on Tier 2 returns warnings
  • Force-override: audit event emitted with required fields; missing force_reason rejects with validation error
  • Lifecycle invariant: enforce-refusal does NOT trigger deregistration / heartbeat stop (state-machine assertions)

Out of scope (v0.2)

  • Generic “any uncommitted file” check (too noisy on this monorepo)
  • CI-side enforcement (downstream — once wrap-time enforcement proves the pattern)
  • Auto-commit on wrap (unsafe — agents must not author commits without operator review)
  • Cross-fleet drift detection in prism_start — separate SPEC if pursued

Risk

  • Adoption: R2 — methodology change affecting every wrap and checkpoint. Backward-compat preserved via advisory default + force-override.
  • Lifecycle invariant is the highest-risk surface: a buggy preflight that accidentally deregistered a session would strand agents. Test coverage on the lifecycle-state-machine assertion is mandatory before flag flip.

Implementation cautions (per Texi second-pass)

Captured here so they survive the spec → implementation handoff:
  1. Don’t let advisory mode become noisy generic dirty-worktree lint. The path-family AND intent-language match together are the product boundary; either alone is too broad.
  2. Force-override must be durable through partial failures. If TriGraph audit-event write is unavailable when the operator invokes force, return a clear mode-appropriate warning or error rather than silently force-closing without an audit trail.
  3. Keep remote/ahead-behind best-effort only. No network remote fetch on wrap or checkpoint in v0.2 — keep the verb local-only.
  4. Lifecycle test assertions must explicitly verify no calls to releaseController / stopHeartbeat / stopStream / shutdownDaemon occur on enforced refusal. The state-machine test is the load-bearing safety surface.

Cross-references

  • method.wall-break.persistence v0.2 — Element 4 (Wrap-time handoff gate)
  • SPEC-094 v0.3 — first observed instance; postmortem 2bc9bda2
  • ADR-16 — PRISM.md sync pre-flight; force=True semantics modeled on this
  • Postmortem 07e9345e (2026-05-08 second occurrence) — action item satisfied here
  • SPEC-063 — postmortem closed-form template
  • SPEC-095 (Porsche, machine_id launch contract) — distinct workstream; this SPEC renumbered to avoid collision
  • Texi v0.1 review (signal b1eb74a4) — eight required revisions all applied as v0.2

Acceptance criteria for v0.2 → v0.3 promotion

  1. Phase 0 advisory mode shipped + observed for at least 7 days of active multi-agent sessions.
  2. Telemetry shows the warning fired at least 3 times on independent sessions, OR shows zero fires. Zero fires does not auto-flip the flag — Texi + Candi review the telemetry and explicitly elect to flip.
  3. Texi architecture review of v0.2 → v0.3 deltas (lifecycle invariant test coverage, Tier 1 enforcement scope, audit event surface).
  4. Candi governance ratification of v0.3.
  5. MCP-node test coverage per §Tests (porcelain parsing, path normalization, ahead/behind, docs.json diff, graceful degradation).
  6. Backend test coverage per §Tests (path-family matching, reference matching, mode behavior, force-override audit, lifecycle invariant).
Last modified on June 7, 2026