Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions complete/2026/08/ci-status-cloud-runs-drop-point.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
- issue: https://github.com/PyAutoLabs/PyAutoHeart/issues/179
- completed: 2026-08-25
- library-pr: PyAutoHeart#180 (merged a570d0a -> main) — shared with #178, the two halves of one no-gh path
- what shipped: `HEART_CLOUD_CI_DIR` (`$HEART_STATE_DIR/cloud_ci/<Name>.json`) holding `{ts, runs}`, read by `ci_status.cloud_runs()` and consulted by `main` **only when the live fetch already failed**. New `source` field on the sidecar (`"gh"` | `"cloud"`) recording provenance. `ts` optional — file mtime is used when absent.
- why the ordering matters: `gh` is authoritative and is never second-guessed, so on the dev box `error` is empty and the new path never runs. That is what keeps dev-box behaviour byte-for-byte unchanged.
- the insight that made it small: MCP's `list_workflow_runs` returns exactly the `{"workflow_runs": [...]}` snake_case shape `normalize_runs` already expects — verified against the live API — so nothing is reshaped on the way in. Mirrors `test_run.py`'s proven `cloud_validation.json` hand-off rather than inventing a second idiom.
- speed decision that matters most: the tick does **no** network work for this. It only reads the drop point, as it already reads `cloud_validation.json`; population is a separate one-shot leg. ~20 repos of network calls inside the tick loop would blow the <30s budget outright.
- fails closed everywhere: unreadable, malformed, empty, undated-and-un-stat-able, or past `HEART_CLOUD_CI_MAX_AGE` (default 3600s) all degrade to the same `unavailable` a failed fetch produces, never a stale green. A rejected payload appends its reason to the original fetch error rather than vanishing, so "no gh AND the payload was stale" is one readable string.
- validation: 635 tests pass (13 new across `cloud_runs`' freshness/fail-closed paths and `main`'s ordering). End-to-end in a gh-less container against a drop point built from a real MCP response: consumed with `source="cloud"` and the error cleared.
- the finding worth keeping: that same run still declined to report success, because PyAutoFit's `main` advanced between fetching the runs and the tick, so the pre-existing `on_head` guard marked them off-HEAD. The guard working as intended — and the reason the age bound alone is not sufficient. **A payload can be fresh by clock and stale by commit.** The on-HEAD success path is covered by a unit test rather than by rewriting the sha to force a green.
- standing constraint, not solved here: the Actions API needs each repo attached to the session, and attachment can be refused — it was, for one library, during the session that motivated this. Such a repo degrades to `unavailable`, which is correct rather than an error, but it means this half delivers less on mobile than #178 does.

## Original prompt

# Feed ci_status run conclusions from an MCP drop point

Type: feature
Target: PyAutoHeart
Repos:
- PyAutoHeart
Difficulty: medium
Autonomy: supervised
Priority: normal
Status: formalised

# Feed ci_status run conclusions from an MCP drop point

Type: feature
Target: PyAutoHeart
Difficulty: medium
Autonomy: supervised
Priority: normal

## Problem

`heart/checks/ci_status.sh` fetches each repo's recent `main` workflow runs with
`gh api`. On a mobile/cloud session there is no `gh`, so every polled repo
records `status="unavailable"` and CI is unknown per-library.

Observed 2026-08-25 in a cloud session: readiness reported five
`CI status unavailable` rows, all from this one missing binary.

## Why this is small

`gh` is only **transport**, not judgment. The reasoning already lives in
`heart/checks/ci_status.py`, which reads a REST-shaped
`{"workflow_runs": [...]}` payload from stdin (`ci_status.py:113,127`).
`mcp__github__actions_list(list_workflow_runs)` returns exactly that shape,
snake_case keys included — verified against PyAutoLabs/PyAutoHeart. The MCP tools
already emit byte-compatible input for the existing parser, so nothing needs
reimplementing.

The pattern is proven here already: `test_run.py:64-72` defines an MCP-supplied
drop point for precisely this case, and that is what cleared the test-run gap on
mobile. Mirror it rather than inventing a second idiom.

## Change

- Add a per-repo drop point (e.g. `$HEART_STATE_DIR/cloud_ci/<Name>.json`
carrying `{runs, ts}`).
- `check_one_repo_ci` prefers the drop point when present and within an age
bound, falls back to `gh`, then to today's `unavailable`.
- Brain populates it via MCP using the same "bash cannot call MCP, so emit a plan
the agent executes" hand-off `rehearse.sh` already uses.

## Keeping it fast

1. **The tick never does network work for this.** Population is a separate
one-shot leg; the `<30s` tick only *reads* the drop point, exactly as it reads
`cloud_validation.json`. 20 repos of network calls inside the tick loop would
blow the budget outright.
2. **Default scope is the five release-gate libraries**, not the whole poll set —
5 MCP calls, not 20. A full sweep stays available but opt-in.
3. **No MCP call for the sha.** That half is `git ls-remote` (separate task),
which halves the round trips and needs no repo attachment.
4. **Age-bound the drop point** so stale data never masquerades as fresh.

Note the operational constraint: the Actions API requires each repo attached to
the session, and attachment can be refused. Degrading to `unavailable` for a repo
that cannot be reached is the correct outcome, not an error.

## Acceptance

- In a cloud session with no `gh`, `pyauto-heart tick && pyauto-heart readiness`
reports CI status for the five release-gate libraries.
- The tick stays inside its `<30s` budget with the drop point populated.
- With neither drop point nor `gh`, behaviour is unchanged from today.
- A drop point past its age bound reads as `unavailable`, never as fresh.

<!-- formalised by the Intake (Conception) Agent on 2026-08-25 from file:/tmp/claude-0/-home-user/1e0c2b0b-8607-5d9d-8871-ab7a769bb699/scratchpad/p2_dropoint.md -->
72 changes: 72 additions & 0 deletions complete/2026/08/ci-status-head-sha-without-gh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
- issue: https://github.com/PyAutoLabs/PyAutoHeart/issues/178
- completed: 2026-08-25
- library-pr: PyAutoHeart#180 (merged a570d0a -> main)
- what shipped: `ci_head_sha` in `heart/checks/ci_status.sh` — the `main` HEAD sha now falls back to an anonymous `git ls-remote` when `gh` cannot supply it. `gh` stays first (unchanged dev-box behaviour, and the only source that can read a private repo); the fallback needs neither authentication nor a session repo attachment, so it works exactly where `gh` cannot.
- why: a cloud `/health` run cleared all three named evidence gaps and readiness still reported STALE on six reasons, every one of them caused by `gh` being absent from the session container. This half clears the one blocking the release gate — `release validation source unconfirmed (current HEADs unknown)`.
- the insight that made it small: `gh` is only **transport**. The judgment already lives in `ci_status.py`, which reads a REST-shaped `{"workflow_runs": [...]}` payload plus a `--head-sha` argument. Nothing needed reimplementing.
- why it works standalone: `build_sidecar` writes `head_sha` unconditionally (`ci_status.py:274`), independent of the runs fetch error — so the CI row keeps reading `unavailable` until the conclusions half lands, but the release gate can already confirm `commit_shas`.
- bounded on purpose: this runs inside the <30s tick. `GIT_TERMINAL_PROMPT=0` stops a private repo blocking on a credential prompt; `timeout` caps a stalled connection.
- portability trap found in review, not in CI: `timeout` is coreutils. macOS has it only as `gtimeout`, and only with coreutils installed. Resolved once at source time to `timeout`/`gtimeout`; where neither exists the fallback is **skipped** rather than run unbounded — a stalled `ls-remote` inside the tick is a worse failure than the empty sha that is already today's answer.
- validation: 635 tests pass (8 new for `ci_head_sha`, stubbing `gh`/`git` on PATH per the `test_verify_install_script.py` precedent). End-to-end in a gh-less container: 25 repos scanned in 3.3s, all five release-gate libraries resolving their real shas, and the source-unconfirmed reason gone from readiness.
- CI caught a real defect, not a flake: the new test defaulted its repo argument to a real owner/name, which the tenant-firewall check correctly rejected for an unlisted organ file. Genericised rather than allowlisted — the allowlist's own comment says never to grow it casually, and `gh`/`git` are both stubbed in that test so a real repo string bought nothing.
- trap for later: a shallow `--depth 1` clone sets `remote.origin.fetch` to `main` only, so a pushed feature branch gets no remote-tracking ref and `@{u}` fails — which reads as "unpushed" to tooling even when the remote branch exists. Widen the refspec before trusting an ahead/behind count in a cloud session.

## Original prompt

# Read ci_status HEAD shas without gh, via git ls-remote

Type: feature
Target: PyAutoHeart
Repos:
- PyAutoHeart
Difficulty: small
Autonomy: supervised
Priority: normal
Status: formalised

# Read ci_status HEAD shas without gh, via git ls-remote

Type: feature
Target: PyAutoHeart
Difficulty: small
Autonomy: supervised
Priority: normal

## Problem

`heart/checks/ci_status.sh` reads each repo's `main` HEAD sha with
`gh api repos/<owner>/<name>/commits/main --jq .sha`. On a mobile/cloud session
`gh` is not installed, so the sha comes back empty. Readiness then cannot
confirm an ingested release-validation report against the live HEADs and reports
`release validation source unconfirmed (current HEADs unknown)`.

Observed 2026-08-25 in a cloud session: all three named evidence gaps were
cleared, yet readiness stayed STALE on this reason alone.

## Change

Read the sha with `git ls-remote <url> refs/heads/main` instead of `gh`, either
as the primary source or as the fallback when `gh` is absent.

This needs no authentication and no session repo attachment — it works on public
repos over the plain git lane. Verified in a cloud session for all five
release-gate libraries, including one whose `add_repo` attachment was refused
outright.

Keep it cheap: `ls-remote` with an explicit ref pattern is a single ref read, no
clone, and it slots into the existing `check_one_repo_ci` parallel fan-out, so
the `<30s` tick budget is unaffected. Preserve the current graceful degradation —
an empty sha on failure, never a fabricated one.

Scope is the sha only. The CI *conclusions* half still needs the Actions API and
is a separate task.

## Acceptance

- With `gh` absent, `pyauto-heart tick` records a real `head_sha` for each polled
repo and readiness no longer emits `release validation source unconfirmed
(current HEADs unknown)`.
- The tick stays inside its `<30s` budget.
- A failed `ls-remote` still yields an empty sha, not a wrong one.

<!-- formalised by the Intake (Conception) Agent on 2026-08-25 from file:/tmp/claude-0/-home-user/1e0c2b0b-8607-5d9d-8871-ab7a769bb699/scratchpad/p1_lsremote.md -->
4 changes: 3 additions & 1 deletion complete/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Token-light navigation over the finished-work records (schema:
only then grep a dated bucket. Curators: edit the band between the CURATED
markers; everything below GENERATED is rebuilt.

1130 records across 7 buckets.
1132 records across 7 buckets.

<!-- CURATED:START -->
## Highlights
Expand Down Expand Up @@ -42,6 +42,8 @@ _(curate hard-won records here — survives regeneration.)_
- [border-relocator-degenerate-pca](2026/08/border-relocator-degenerate-pca.md) — Stabilized near-isotropic border PCA axes with a deterministic axis-aligned branch at a scale-aware eigenvalue…
- [brain-readability-pass](2026/08/brain-readability-pass.md) — auto-closed on merge
- [bump-autonerves-floor](2026/08/bump-autonerves-floor.md)
- [ci-status-cloud-runs-drop-point](2026/08/ci-status-cloud-runs-drop-point.md)
- [ci-status-head-sha-without-gh](2026/08/ci-status-head-sha-without-gh.md)
- [circular-ell-comps-image-gradient](2026/08/circular-ell-comps-image-gradient.md) — Replaced the Sersic image path's Cartesian-to-polar ellipticity conversion with an algebraically equivalent Ca…
- [claude-session-python-312-default](2026/08/claude-session-python-312-default.md)
- [cli-noise-autonerves-batch](2026/08/cli-noise-autonerves-batch.md)
Expand Down
Loading