Conversation
…g — Aaron 2026-04-26 ask
Aaron 2026-04-26: *"anything we can do to prevent it in the first place?"* after I caught the row-ordering bug yet again this tick. The bug shape: Edit tool's old_string=existing-line pattern tends to insert NEW row BEFORE the matched line, producing reverse-chronological order in `docs/hygiene-history/loop-tick-history.md`.
This commit ships three additive layers:
1. `tools/hygiene/check-tick-history-order.sh` — CI gate that validates the LAST row in the file is the LATEST timestamp. This catches the specific bug shape (new row inserted before previous last row) without requiring history-rewrite (Otto-229 forbids editing prior rows). Two-tier:
- Default: quiet on historical strict-order violations; only fails on the last-row check
- `--strict`: reports historical violations too (advisory, for human spelunking)
2. `tools/hygiene/append-tick-history-row.sh` — helper that wraps `cat >> file` (the canonical chronological-tail-append pattern) so the bug shape can't occur via this entrypoint. Validates new row's timestamp >= latest existing.
3. `.github/workflows/gate.yml` `lint-tick-history-order` job — wires the check into CI so violations fail fast at PR-open time, not at the "I caught it again" moment.
Self-tests:
- shellcheck --severity=style: pass
- actionlint on gate.yml: pass
- check script default mode: clean 2-line output
- check script --strict mode: 32-line detail (3 historical violations + bug-pattern absent)
- append helper happy path: works
- append helper rejects backwards timestamp: works
Aaron's specific guidance ("we might should allow this one override if it exists a lot") implemented as the default-quiet posture: noisy historical-violation reporting is opt-in via --strict, since Otto-229 forbids the only way to fix history.
This is Otto-339 anywhere-means-anywhere applied to discipline-enforcement: enforce at the layer that catches all paths (CI gate at commit time), not at the input-tool layer (which would require every agent's vigilance and miss future tools).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: de361d1e33
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| LATEST_TS=$( | ||
| grep -oE '^\| [0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z' "$TICK_FILE" \ | ||
| | sed 's/^| //' \ | ||
| | sort \ | ||
| | tail -1 |
There was a problem hiding this comment.
Handle empty timestamp history before computing latest row
The helper exits before appending when loop-tick-history.md contains zero ISO timestamp rows (for example, a freshly bootstrapped file or a post-archive reset with only headers). With set -euo pipefail, the grep in the LATEST_TS pipeline returns status 1 on no matches, which aborts the script before cat >> runs, so the tool cannot append the first valid row at all.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds structural safeguards to prevent the recurring tick-history row-ordering regression by introducing a dedicated order-check script, an append helper, and wiring the check into CI.
Changes:
- Add
tools/hygiene/check-tick-history-order.shto verify the last tick-history row is the latest timestamp (with optional--strictadvisory reporting). - Add
tools/hygiene/append-tick-history-row.shhelper to safely append new tick-history rows while enforcing non-decreasing timestamps. - Add a new
lint-tick-history-orderjob in.github/workflows/gate.ymlto run the check in CI.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tools/hygiene/check-tick-history-order.sh | New validator script for tick-history ordering with a CI-gating “last row is latest” check. |
| tools/hygiene/append-tick-history-row.sh | New helper script to append tick-history rows while validating timestamp monotonicity. |
| .github/workflows/gate.yml | Adds a CI job to run the new tick-history order validator. |
| # Append using heredoc (the whole point of this script — bash | ||
| # heredoc is the canonical chronological-tail-append pattern) | ||
| cat >> "$TICK_FILE" << EOF | ||
| $ROW | ||
| EOF |
There was a problem hiding this comment.
P0 (security): The heredoc uses an unquoted delimiter (<< EOF), so bash will perform expansion (incl. command substitution) on the here-doc body. If the provided row text ever contains $(...) / backticks / $var, it could execute or mutate content unexpectedly. Prefer appending via printf '%s\n' "$ROW" >> "$TICK_FILE", or use a single-quoted heredoc delimiter (<<'EOF') and emit the row without additional expansion.
| # Append using heredoc (the whole point of this script — bash | |
| # heredoc is the canonical chronological-tail-append pattern) | |
| cat >> "$TICK_FILE" << EOF | |
| $ROW | |
| EOF | |
| # Append the row literally so caller-provided content is treated | |
| # as data, not shell syntax. | |
| printf '%s\n' "$ROW" >> "$TICK_FILE" |
| # What this checks: | ||
| # - Every row matching the ISO-8601 timestamp prefix | ||
| # `| YYYY-MM-DDTHH:MM:SSZ (...)` is extracted in file order | ||
| # - Timestamps must be non-decreasing (allows duplicates from | ||
| # close ticks; forbids out-of-order) | ||
| # - Reports first violation with surrounding context and | ||
| # exits non-zero on failure | ||
| # |
There was a problem hiding this comment.
P1 (documentation): The file header and mid-file comments claim this script "validates" full non-decreasing order and "exits non-zero on failure", but the implementation only gates CI on the PRIMARY check (last row is the latest timestamp). Strict-order violations are advisory (and only printed when --strict is set). Please align the comments at the top and around the two-tier section with the actual behavior so readers don’t assume CI enforces full ordering.
| # --strict: also report (advisory) historical strict-order violations | ||
| # anywhere in the file. Default is quiet because Otto-229 forbids | ||
| # editing prior rows so historical disorder cannot be repaired — | ||
| # reporting it on every CI run is noise. Aaron 2026-04-26: "we | ||
| # might should allow this one override if it exists a lot." |
There was a problem hiding this comment.
P2 (documentation/behavior mismatch): This comment says the default mode is "quiet" on historical strict-order violations to avoid CI noise, but later the script still prints an advisory summary when violations > 0 even without --strict. Either change the behavior to be fully quiet in default mode, or adjust this comment to match the actual output contract.
| # --strict: also report (advisory) historical strict-order violations | |
| # anywhere in the file. Default is quiet because Otto-229 forbids | |
| # editing prior rows so historical disorder cannot be repaired — | |
| # reporting it on every CI run is noise. Aaron 2026-04-26: "we | |
| # might should allow this one override if it exists a lot." | |
| # --strict: report (advisory) historical strict-order violations | |
| # anywhere in the file with full detail. Default mode does not | |
| # fail on that historical disorder, but it may still print a brief | |
| # advisory summary when such violations exist. Otto-229 forbids | |
| # editing prior rows, so repeated historical findings are mostly | |
| # noise unless explicitly requested. Aaron 2026-04-26: "we might | |
| # should allow this one override if it exists a lot." |
| # reverse-chronological order. Aaron 2026-04-26 asked for | ||
| # structural prevention after I caught this bug at least | ||
| # three times across recent ticks. The check is the | ||
| # "fail-fast at commit/push time" mechanism that doesn't | ||
| # rely on each agent's vigilance — Otto-339 anywhere-means- | ||
| # anywhere applied to discipline-enforcement: enforce at |
There was a problem hiding this comment.
P1 (codebase convention): This workflow comment includes direct contributor name attribution ("Aaron …"). Repo operational rule is to avoid names on current-state surfaces like workflows/scripts and use role references (e.g., "human maintainer"), reserving names for the explicitly enumerated history surfaces (docs/AGENT-BEST-PRACTICES.md:284-347). Please rewrite these attributions here (and in the new hygiene scripts) to role-refs.
Summary
Aaron 2026-04-26 — "anything we can do to prevent it in the first place?" — after I caught the recurring tick-history row-ordering bug yet again this tick.
Bug shape: Edit tool's
old_string=existing-linepattern tends to insert NEW row BEFORE the matched line, producing reverse-chronological order in `docs/hygiene-history/loop-tick-history.md`. I've caught this at least three times across recent ticks; relying on agent vigilance hasn't worked.Structural fix: three additive layers.
1. CI gate — `tools/hygiene/check-tick-history-order.sh`
Validates that the LAST row in the file is the LATEST timestamp. Catches the specific bug shape (new row inserted before previous last row) without requiring history-rewrite (Otto-229 forbids editing prior rows).
Two-tier:
Per Aaron's specific guidance — "we might should allow this one override if it exists a lot" — historical-violation noise is opt-in.
2. Append helper — `tools/hygiene/append-tick-history-row.sh`
Wraps `cat >> file` (the canonical chronological-tail-append pattern) so the bug shape can't occur via this entrypoint. Validates new row's timestamp >= latest existing.
3. CI workflow — `gate.yml` job `lint-tick-history-order`
Wires the check into CI so violations fail fast at PR-open time.
Why this is structural, not just another rule
Per Otto-339 anywhere-means-anywhere applied to discipline-enforcement: enforce at the layer that catches all paths (CI gate at commit time), not at the input-tool layer (which would require every agent's vigilance and miss future tools / future agents).
Adding a rule in CLAUDE.md or a memory file would be Tier-1 (language layer) — Aaron's question was about structural prevention, which means moving the discipline to a mechanism layer.
Self-tests
What this DOES NOT do
Test plan
🤖 Generated with Claude Code