diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f065e58c4..5aa01ea2d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -84,6 +84,19 @@ repos: pass_filenames: true stages: [pre-commit] + # OMN-13023 (retro B-11): UTC timestamp discipline in handoff/evidence docs. + # Rejects Z-suffixed clock times cited next to file-mtime markers (ls -l, + # mtime, Finder, last modified) — local mtimes relabeled as UTC manufactured + # a false outage narrative on 2026-06-11 (PROCESS_FAILURE_RETRO.md §5.1). + # Hard fail — no warn-only (CLAUDE.md Rule #5). Escape: 'utc-ok: '. + - id: check-utc-timestamp-discipline + name: Reject Z-suffixed times adjacent to mtime citations (OMN-13023) + entry: .pre-commit-hooks/check-utc-timestamp-discipline.sh + language: script + types: [markdown] + pass_filenames: true + stages: [pre-commit] + # OMN-9730: DGM-Phase4 — mechanical block on skip-deploy-gate bypass tokens. # Rejects any staged file containing [skip-deploy-gate:] without an explicit # approval receipt (# skip-token-allowed: ). diff --git a/.pre-commit-hooks/check-utc-timestamp-discipline.sh b/.pre-commit-hooks/check-utc-timestamp-discipline.sh new file mode 100755 index 0000000000..7d187859ba --- /dev/null +++ b/.pre-commit-hooks/check-utc-timestamp-discipline.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +# +# OMN-13023 (retro B-11): Reject Z-suffixed timestamps adjacent to file-mtime +# citations in handoff/evidence documents. +# +# Failure class this prevents (PROCESS_FAILURE_RETRO.md §5.1, 2026-06-11): +# an evening handoff transcribed local-EDT `ls -l` mtimes with a `Z` suffix, +# manufacturing a false "16:00Z mass worker death / 4h undetected" outage +# narrative that a later investigation consumed as fact. UTC timestamps must +# come from `date -u` or epoch conversion — never from local-time file +# listings relabeled as UTC. +# +# Matches files: +# docs/handoffs/**/*.md +# docs/evidence/**/*.md +# +# Fires when a single line contains BOTH: +# (a) an mtime-citation marker: `ls -l`, `ls -la`, `mtime`, `Finder`, +# `last modified`, `modified at`, `modification time` +# (b) a Z-suffixed clock time (e.g. `16:00Z`, `2026-06-11T16:00:12Z`) +# +# Escape hatch (same line): `utc-ok: ` — use ONLY when the timestamp +# was independently produced by `date -u`/epoch conversion and the mtime +# marker is incidental prose. Cite the conversion. +# +# Fails with: +# UTC_MTIME_MISLABEL: : — Z-suffixed time cited next to an mtime +# +# CLAUDE.md Rule #5 (enforcement, not detection). No warn-only mode. + +set -euo pipefail + +TICKET_REF="OMN-13023" +MTIME_MARKER='(ls -la?|mtime|Finder|last modified|modified at|modification time)' +Z_TIME='[0-9]{2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]+)?Z' +ESCAPE='utc-ok:' + +FOUND_VIOLATION=0 + +for file in "$@"; do + case "$file" in + *.md) ;; + *) continue ;; + esac + + # Only handoff/evidence documents + case "$file" in + docs/handoffs/*.md|docs/evidence/*.md|\ + docs/handoffs/*/*.md|docs/evidence/*/*.md|\ + */docs/handoffs/*.md|*/docs/evidence/*.md|\ + */docs/handoffs/*/*.md|*/docs/evidence/*/*.md) ;; + *) continue ;; + esac + + # Read from staged index when available; fall back to working tree for self-test + if git cat-file -e ":$file" 2>/dev/null; then + content="$(git show ":$file")" + elif [[ -f "$file" ]]; then + content="$(cat "$file")" + else + continue + fi + + lineno=0 + while IFS= read -r line; do + lineno=$((lineno + 1)) + if grep -qE "$ESCAPE" <<<"$line"; then + continue + fi + if grep -qiE "$MTIME_MARKER" <<<"$line" && grep -qE "$Z_TIME" <<<"$line"; then + echo "UTC_MTIME_MISLABEL: ${file}:${lineno} — Z-suffixed time cited next to an mtime marker (${TICKET_REF})" >&2 + echo " ${line}" >&2 + echo " Local file mtimes are NOT UTC. Source the timestamp from 'date -u' or epoch conversion," >&2 + echo " or drop the Z suffix and label the timezone explicitly. Escape hatch: 'utc-ok: '." >&2 + FOUND_VIOLATION=1 + fi + done <<<"$content" +done + +exit "$FOUND_VIOLATION" diff --git a/tests/hooks/fixtures/utc_timestamp_discipline/invalid_mtime_z_suffix.md b/tests/hooks/fixtures/utc_timestamp_discipline/invalid_mtime_z_suffix.md new file mode 100644 index 0000000000..69014be72e --- /dev/null +++ b/tests/hooks/fixtures/utc_timestamp_discipline/invalid_mtime_z_suffix.md @@ -0,0 +1,9 @@ +# Evening handoff + +## Infra Health + +All lanes nominal. + +## Timeline + +Worker death detected: file mtime 16:00Z per ls -l of the workflow directory. diff --git a/tests/hooks/fixtures/utc_timestamp_discipline/valid_date_u_sourced.md b/tests/hooks/fixtures/utc_timestamp_discipline/valid_date_u_sourced.md new file mode 100644 index 0000000000..dd061e7d6a --- /dev/null +++ b/tests/hooks/fixtures/utc_timestamp_discipline/valid_date_u_sourced.md @@ -0,0 +1,10 @@ +# Evening handoff + +## Infra Health + +All lanes nominal. + +## Timeline + +Worker death boundary: 2026-06-11T20:00:12Z (sourced from `date -u` against wf_*.json epoch records). +The workflow directory listing was taken separately; mtimes recorded as local EDT without a Z suffix. diff --git a/tests/hooks/fixtures/utc_timestamp_discipline/valid_escape_hatch.md b/tests/hooks/fixtures/utc_timestamp_discipline/valid_escape_hatch.md new file mode 100644 index 0000000000..537be565c6 --- /dev/null +++ b/tests/hooks/fixtures/utc_timestamp_discipline/valid_escape_hatch.md @@ -0,0 +1,9 @@ +# Evening handoff + +## Infra Health + +All lanes nominal. + +## Timeline + +Checkpoint at 20:05:00Z near the mtime boundary (utc-ok: timestamp from epoch conversion of wf_1234.json, mtime mention is incidental). diff --git a/tests/hooks/test_check_utc_timestamp_discipline.py b/tests/hooks/test_check_utc_timestamp_discipline.py new file mode 100644 index 0000000000..8e4a15dee1 --- /dev/null +++ b/tests/hooks/test_check_utc_timestamp_discipline.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""Tests for check-utc-timestamp-discipline.sh — OMN-13023 (retro B-11). + +Verifies that handoff/evidence documents citing Z-suffixed clock times adjacent +to file-mtime markers are rejected, that date-u-sourced timestamps pass, that +the ``utc-ok:`` escape hatch works, and that non-handoff/evidence docs are +ignored. Failure class: PROCESS_FAILURE_RETRO.md §5.1 (local-EDT mtimes +relabeled as UTC manufactured a false outage narrative on 2026-06-11). +""" + +import os +import shutil +import subprocess +import tempfile +from pathlib import Path + +import pytest + +HOOK = ( + Path(__file__).parent.parent.parent + / ".pre-commit-hooks" + / "check-utc-timestamp-discipline.sh" +) +FIXTURES = Path(__file__).parent / "fixtures" / "utc_timestamp_discipline" + + +def run_hook(fixture_file: Path, fake_path: str) -> tuple[int, str]: + """Run the hook against a fixture file at a spoofed repo-relative path.""" + tmp_dir = tempfile.mkdtemp() + try: + spoof_path = Path(tmp_dir) / fake_path + spoof_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(fixture_file, spoof_path) + result = subprocess.run( + ["bash", str(HOOK), str(spoof_path)], + capture_output=True, + check=False, + text=True, + env={**os.environ, "GIT_DIR": "/dev/null"}, + ) + return result.returncode, result.stderr + finally: + shutil.rmtree(tmp_dir, ignore_errors=True) + + +@pytest.mark.unit +def test_rejects_z_suffix_adjacent_to_mtime_in_handoff() -> None: + """A Z-suffixed time on the same line as an mtime marker must be rejected.""" + rc, stderr = run_hook( + FIXTURES / "invalid_mtime_z_suffix.md", + fake_path="docs/handoffs/2026-06-11-evening-handoff.md", + ) + assert rc != 0, "Expected hook to reject Z-suffixed time next to mtime citation" + assert "UTC_MTIME_MISLABEL" in stderr + + +@pytest.mark.unit +def test_rejects_same_content_in_evidence_dir() -> None: + """The same violation under docs/evidence/ must also be rejected.""" + rc, stderr = run_hook( + FIXTURES / "invalid_mtime_z_suffix.md", + fake_path="docs/evidence/2026-06-11-incident/timeline.md", + ) + assert rc != 0 + assert "UTC_MTIME_MISLABEL" in stderr + + +@pytest.mark.unit +def test_passes_date_u_sourced_timestamps() -> None: + """Z-suffixed times sourced from date -u with no same-line mtime marker pass.""" + rc, stderr = run_hook( + FIXTURES / "valid_date_u_sourced.md", + fake_path="docs/handoffs/2026-06-11-evening-handoff.md", + ) + assert rc == 0, f"Expected pass, got stderr: {stderr}" + + +@pytest.mark.unit +def test_escape_hatch_utc_ok_suppresses_finding() -> None: + """A line carrying 'utc-ok: ' is exempt.""" + rc, stderr = run_hook( + FIXTURES / "valid_escape_hatch.md", + fake_path="docs/handoffs/2026-06-11-evening-handoff.md", + ) + assert rc == 0, f"Expected escape hatch to pass, got stderr: {stderr}" + + +@pytest.mark.unit +def test_ignores_non_handoff_non_evidence_docs() -> None: + """Docs outside docs/handoffs/ and docs/evidence/ are not in scope.""" + rc, stderr = run_hook( + FIXTURES / "invalid_mtime_z_suffix.md", + fake_path="docs/architecture/some-design.md", + ) + assert rc == 0, f"Expected out-of-scope doc to pass, got stderr: {stderr}" + + +@pytest.mark.unit +def test_ignores_non_markdown_files() -> None: + """Non-markdown files are ignored even under docs/handoffs/.""" + rc, _ = run_hook( + FIXTURES / "invalid_mtime_z_suffix.md", + fake_path="docs/handoffs/notes.txt", + ) + assert rc == 0 + + +@pytest.mark.unit +def test_hook_is_executable() -> None: + """The hook script must carry the executable bit (pre-commit language: script).""" + assert os.access(HOOK, os.X_OK), f"{HOOK} is not executable"