I1520 — A step that runs out of time says so itself
Make a CI step that exceeds its time report a failure naming the bound and the elapsed time, instead of being killed by its job and reported by GitHub as a cancellation that every reader in this repository treats as not-a-failure.
DONE wave 0 · p1 · implementation profile · parallel safe
Part of development-runtime — The runtime has a development surface.
Objective
Make a CI step that exceeds its time report a failure naming the bound and the elapsed time, instead of being killed by its job and reported by GitHub as a cancellation that every reader in this repository treats as not-a-failure.
Why
The site job's 'audit the UI in a browser' step has been killed at exactly its job's 50-minute timeout-minutes bound on master at 6a8d47a47 (01:14 to 02:04), at 70685d473 (23:13 to 00:03), and on both runs of the integration branch #310 — three consecutive runs, each to the second, and nothing was ever red. GitHub reports a job killed by its own bound as `cancelled`, and `cancelled` is what the gates, the summaries and three sessions in one night all read as 'not a failure'. I read it that way myself an hour before measuring it.\n\nThe cause was in the instrument. scripts/ci/timed computed the elapsed time after the command returned, so a command that never returned was never measured and no row was ever written to say otherwise. The only thing that could have reported the problem was the thing the problem prevented from running. That is the class: a verdict with no measurement behind it, the same family as english-only-check dying on its own regex and announcing that every file is English, and coverage-differential being declared, planned, named in a job's if: and invoked by no step.
Current state
All 13 timeout-minutes in .github/workflows/validate.yml are job-level; no step carries a bound of its own. scripts/ci/timed measures after the fact and cannot report a step it never saw finish. The UI audit is over its job's bound and has been for as long as the run retention window goes back.
Desired state
Every timed step declares a bound smaller than its job's, so a runaway step always loses the race to its own message; the timing row is written even when the bound fires, because that is exactly when it is the only evidence; and a gate holds the relation so a new step cannot be added without one.
Scope
- scripts/ci
- .github/workflows
- .ai/repo/ci
- test/cases
Out of scope
- Making the UI audit faster, or deciding what its bound should be. This converts a silent kill into a named failure; what the right bound is, is a separate and deliberate decision.
- The sum of a job's step bounds. Several steps can still add up past their job's bound and be killed together; the job bound remains the backstop it always was.
Dependencies
None. This issue is a root of the graph.
Acceptance criteria
- A step over its bound exits non-zero with a message naming the bound and the elapsed time
- The timing row is written when the bound fires, not only when the command returns
- Every scripts/ci/timed call declares a bound strictly smaller than its job's timeout-minutes
- A gate refuses a timed step with no bound, and one whose bound reaches its job's
Validation
- bash scripts/ci/step-bound-check
- bash test/run.sh 349_a_step_reports_its_own_time
Evidence required
- a_step_over_its_bound_reports_it
- no_step_can_be_killed_by_its_job_first
Evidence
| covers | type | command | result | at commit |
|---|---|---|---|---|
| a_step_over_its_bound_reports_it | test | bash test/run.sh 349_a_step_reports_its_own_time | ok at b4a781c668: a step that runs out of time reports it and its timing row is written; the fix is b3e261428b | b4a781c |
| no_step_can_be_killed_by_its_job_first | test | scripts/ci/step-bound-check | exit 0 at b4a781c668: every timed step is bounded under its job | b4a781c |
Risk
This makes the site job red rather than cancelled, loudly and on every run, until the UI audit finishes inside its bound or somebody raises the bound with a reason. That is the intended effect and it is worth saying plainly: a silent kill is exchanged for a visible failure, not for a fix.
Timeline
- started
- 2026-09-15
- verified
- 2026-10-07T13:27:27Z
- completed
- 2026-10-07T13:27:30Z
Those three fields, the evidence above and the state of the dependencies are all the status is made of. There is no status field to disagree with them.
Canonical record: .ai/repo/project/issues/I1520.yaml. Read it back with majordomus plan show I1520.