Skip to content

Live runner: normalized feed input and per-action webhook routing - #320

Merged
luisleo526 merged 29 commits into
mainfrom
lv/runner-routing
Oct 3, 2026
Merged

luisleo526 merged 29 commits into
mainfrom
lv/runner-routing

Conversation

@luisleo526

@luisleo526 luisleo526 commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The native live runner pineforge-live reads only the normalized PineForge feed protocol and routes each order action to a webhook target. The engine computes. It never connects to an exchange or ingests fills; venue execution, reconciliation and risk belong to the consumer, and runner/README.md now says so in a short boundary section.

  • Feed input: the normalized protocol only.
    • Tick mode: contiguous seq plus the time completeness event.
    • Bar mode: confirmed 1-minute bars.
    • Sources: stdin, a file, or the user's own normalized feed over HTTP or WebSocket.
    • The in-runner exchange parser plugins are removed: --parser / --parser-config, include/pineforge/live_parser.h, the demo parser, its tests and the CI wiring. Translating raw exchange messages belongs in a separate feed adapter. A raw exchange message is refused with guidance. README tables give the field types; an optional trade_count on bars is accepted.
  • Reliable WebSocket input. libcurl 8.14.1 or newer is required, and an unfinished message is never committed.
  • Per-action webhook routing.
    • --webhook-routes sends each action to one target. Rules match on order id, kind and side, first match wins, with a default target and a per-target HMAC.
    • A target can be null (journal only), and a runner can run without any webhook: pineforge-live actions --follow reads the journal.
    • Delivery works like an alert. Each action is sent once, in order. An error is shown and never blocks later actions, with a bounded in-flight cap and transport-only retries. Every attempt is an append-only delivery-log row. Offline redeliver --deployment re-sends failed actions, and status reports per target.
    • Without routes, the v1 payload and the event-id idempotency key are unchanged.
  • CI: one pinned libcurl. The kernel verification profile deliberately builds the live runner against the source-free kernel library (docs/ci.md: "live runner enabled, Pine source layer OFF"), WebSocket rows included. Its CI job now uses the same pinned libcurl 8.14.1 as native-live, from one shared definition. The 8.14.1 floor is not lowered.
  • Sanitizer verification of the runner: ASan/UBSan and ThreadSanitizer profiles.

Breaking changes (CHANGELOG)

  • The parser plugin interface (live_parser.h, --parser) is removed. Migration: run an external feed adapter that emits PineForge feed events.
  • Delivery changes for existing --webhook-url users:
    • HTTP error responses are final;
    • the exit status no longer reflects failed deliveries;
    • --max-attempts now caps transport retries;
    • the default timeouts are 2 s to connect and 5 s in total;
    • --webhook-url is optional.
      The ledger migrates one way.

Verification

  • Verification bundles on the head tree: preflight, release, sanitizers, docs, debug, kernel; corpus parity 312/312. Posted as pineforge/verify.
  • Full-population parity sweep, expected neutral because the engine library is unchanged. Posted as pineforge/parity.
  • Native profile with WebSockets 798/798, live ASan/UBSan 13/13, live ThreadSanitizer 13/13, and the routing and recovery E2Es 13/13, including SIGKILL recovery, receiver isolation and the migration of an older ledger.
  • Two independent reviews; all findings fixed or recorded.

🤖 Generated with Claude Code

assert len(main_receiver.rows) == 4
print('PASS SIGKILL between commit/send and mid-request resumes each unsent action once after replay', flush=True)

deployment = query(crash_ledger, 'SELECT identity FROM metadata')[0][0]
self.send_response(status)
self.send_header('Content-Length', '0')
self.end_headers()
except (BrokenPipeError, ConnectionResetError):
self.connection.settimeout(20)
while self.connection.recv(4096):
pass
except OSError:
Remove the parser-plugin advertisement introduced by 5f3299d now that normalized feed adapters define the runner boundary.
@luisleo526
luisleo526 merged commit dbd17b3 into main Oct 3, 2026
17 checks passed
@luisleo526
luisleo526 deleted the lv/runner-routing branch October 3, 2026 22:13
luisleo526 added a commit that referenced this pull request Oct 4, 2026
The 1.1.0 lead now says how state-hash values change while the epoch and
the domain tags stay: a run whose fills an adapter change alters hashes
to new values, and three changes fold more state, so a run can hash
differently from 1.0.1 with byte-identical trades: #315 (an unbatched
same-bar entry request and placement; the stream report's terminal
re-mark leaves the hashed extremes; witness row Stream/0/1 re-pinned),
#316 (an opening stop's next waypoint; Random44/0/0 and /1 re-pinned)
and #319 (tick-volume state, every existing pin kept).

In the runner tooling bullet, "No versioned engine PF_API export ...
changes" and "Plugin-free ledger identity bytes remain unchanged" read
as release-wide; they now speak for #320 itself and for a strategy
without the checked settings calls. The Report keys intro points at the
report schema page. The terminal-quote bullet names the chart feed that
PINEFORGE_RUN_REPORT_CHART_QUOTE points at, and two runner bullet
headings are re-wrapped.

Replaces text introduced by a80485c (this branch's first commit): the
lead's "A run whose fills one of the Pine adapter changes below alters
hashes to new values, and [#316] adds one placement field to the
adapter's hashed state, for which two witnesses with byte-identical
trades were re-pinned.", the Report keys intro and the terminal-quote
harness clause; and text introduced by dbd17b3 (#320): "No versioned
engine PF_API export, native C++ surface, script ABI epoch or engine
behavior changes." and "Plugin-free ledger identity bytes remain
unchanged".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
luisleo526 added a commit that referenced this pull request Oct 4, 2026
* docs: release notes and version scoping for v1.1.0

v1.1.0 is a minor release: <pineforge/pineforge.h> declares six more
functions, the checked settings calls that a strategy generated by
pineforge-codegen 1.1.0 exports (#317). It pairs with pineforge-codegen
1.1.0. PF_ABI_VERSION stays 4 (pineforge.h only gains declarations;
native_c_api.h and the native C++ host headers are unchanged) and the
script ABI epoch stays engine_script_run_v19.

- CHANGELOG.md: "## Unreleased" becomes "## 1.1.0 — 2026-10-04": a lead
  paragraph, one bullet per change a user can observe since v1.0.1
  (#312-#324), each linking its PR, a Report keys subsection
  (docker/run_json.py v1.0.1 -> v1.1.0) and a Migration subsection. The
  three Unreleased bullets stay; the lot-grid bullet now gives both
  failure messages.
- README.md: pineforge-codegen==1.1.0, the v1.1.0 tarballs, the pairing
  sentence, the stream known issue's scope, the benchmark refresh line,
  the release scoreboard sentence (releases[1.1.0] markers) and a v1.1.0
  line in Releases. The public corpus sweep's "311 excellent + 1 declared
  anomaly" is scoped to the v1.0.1 library it was measured on. Facts
  rendered from the facts file that adds releases[1.1.0]; the active
  scoreboard markers move to baseline
  pineforge-parity-baseline-20261003-engine-dbd17b38.
- docs/pages/install.md: the v1.1.0 tarballs and the hub tag
  engine1.1.0-codegen1.1.0.
- docs/pages/public-contract.md: 1.1.0's release line, engine v1.1.0 with
  codegen 1.1.0 among the supported pairs, and the four C-boundary rows
  that were planned for 1.1.0 are not in it.
- docs/pages/streaming.md: the known issue's scope includes v1.1.0.

Lines that state what an earlier version shipped or did stay.

Replaces text introduced by dc74e63 (#318): README.md's "Release 1.0.1
still grades ... until the next release" sentence, now 1.1.0's; by
d093560 (#290): public-contract.md's "Four of its rows are planned for
1.1.0"; by 783bb39 (#322): CHANGELOG.md's "The engine library is
unchanged."; by dbd17b3 (#320): CHANGELOG.md's "The engine is
unchanged."; by b2a578c (#300): README.md's "this repository's sweep:
311 excellent + 1 declared anomaly"; by 271d687 (#311): README.md's
"its releases 1.0.0 and 1.0.1 do".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: state the 1.1.0 hash folds and scope two runner sentences

The 1.1.0 lead now says how state-hash values change while the epoch and
the domain tags stay: a run whose fills an adapter change alters hashes
to new values, and three changes fold more state, so a run can hash
differently from 1.0.1 with byte-identical trades: #315 (an unbatched
same-bar entry request and placement; the stream report's terminal
re-mark leaves the hashed extremes; witness row Stream/0/1 re-pinned),
#316 (an opening stop's next waypoint; Random44/0/0 and /1 re-pinned)
and #319 (tick-volume state, every existing pin kept).

In the runner tooling bullet, "No versioned engine PF_API export ...
changes" and "Plugin-free ledger identity bytes remain unchanged" read
as release-wide; they now speak for #320 itself and for a strategy
without the checked settings calls. The Report keys intro points at the
report schema page. The terminal-quote bullet names the chart feed that
PINEFORGE_RUN_REPORT_CHART_QUOTE points at, and two runner bullet
headings are re-wrapped.

Replaces text introduced by a80485c (this branch's first commit): the
lead's "A run whose fills one of the Pine adapter changes below alters
hashes to new values, and [#316] adds one placement field to the
adapter's hashed state, for which two witnesses with byte-identical
trades were re-pinned.", the Report keys intro and the terminal-quote
harness clause; and text introduced by dbd17b3 (#320): "No versioned
engine PF_API export, native C++ surface, script ABI epoch or engine
behavior changes." and "Plugin-free ledger identity bytes remain
unchanged".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: #315's range-end change folds less, not more

The 1.1.0 lead said three changes "fold more state"; #315's range-end
change instead stops folding a stream report's terminal re-mark into the
hashed extremes. The sentence now says the three changes change what the
hashes fold, and names Stream/0/1 as a re-pinned witness row without
claiming it is #315's only one.

Replaces text introduced by 66be240 (this branch's second commit): "Three
changes also fold more state" and "re-pinning one witness row
(`Stream/0/1`, whose recorded-row digest alone moves)".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: fold #325 and #327 into 1.1.0, add State hashes, amend the hash rule

#325 (confirmed-bar streams compute what the batch computes) and #327
(docker/run_json.py --symbol-feeds) merged before the release notes, so
their Unreleased bullets join the 1.1.0 section, rewritten and linked; no
Unreleased heading stays.

- CHANGELOG.md: the lead names the stream fix and, in the pairing
  sentence, codegen 1.1.0's request discovery (transpile_full()["requests"],
  codegen #164). #325's bullet says what a stream user sees; #327's bullet
  and its Report keys entry (applied_runtime.symbol_feeds, the
  --symbol-feeds: failure messages). A new State hashes section lists the
  four recipe additions since v1.0.1 (#315, #316, #319, #325; none removed,
  renamed or re-encoded; domain tags unchanged). #315's range-end
  equity-extreme change moves to the Pine adapter parity bullet as a
  behaviour fix. The #319 bullet named the broker-state hash; the change is
  in the native continuation hash, which the broker-state hash folds. The
  known-issue bullet now says it is fixed in 1.1.0 (#325). Migration gains
  a line on stream results and hashes.
- README.md: the known issue is fixed in 1.1.0 (#325); the v1.1.0 Releases
  line gains the stream fix, request discovery and the harness's
  multi-symbol request.security feeds. Fact markers rendered from the facts
  export that maps releases[1.1.0] to
  pineforge-parity-baseline-20261004-engine-7b596622 (7,951 excellent / 38
  strong of 7,989); the active scoreboard moves to the same baseline, here
  and in docs/pages/contributing-llm.md.
- docs/pages/streaming.md: the known issue affected v1.0.0 and v1.0.1 and
  is fixed in 1.1.0.
- docs/pages/public-contract.md: the state-hash rule, amended by the
  owner's ruling for 1.1.0. It covers the recipe only: a minor release may
  add a hashed field (domain tags unchanged, the added field changing no
  trade or report, disclosed under State hashes); removing, renaming or
  re-encoding a hashed field is a new epoch and a major release; behaviour
  changes follow the normal release and parity rules. The first uses are
  #315 and #316. The semver rule is unchanged.

Replaces text introduced by 7b59662 (#325): CHANGELOG.md's
"Confirmed-bar streams compute what the batch computes:" bullet, and the
"Fixed on main (#325); included in the next release." lines of README.md
and streaming.md; by 4f2a475 (#327): CHANGELOG.md's "Harness symbol
feeds:" bullet; by d093560 (#290): public-contract.md's "State-hash
values are stable within the epoch." and "a new recipe is a new epoch and
a major release"; by a80485c (this branch's first commit): the "Known
issue, as in v1.0.0 and v1.0.1" bullet, streaming.md's "(v1.0.0, v1.0.1,
v1.1.0)", the #319 bullet's "A tick stream's broker-state hash folds the
new accumulator state only where it adds information, so the existing
tick, bar and batch hash pins hold.", the Report keys failure-output
bullet and the v1.1.0 Releases line; by 66be240 and 83d64dc: the lead's
state-hash sentences ("Three changes also change what the hashes fold");
and, as git attributes them through this branch's merge of main, by
e59c09f: README.md's v1.1.0 Releases line and streaming.md's
"(v1.0.0, v1.0.1, v1.1.0)".

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
luisleo526 added a commit that referenced this pull request Oct 7, 2026
* Split native CI profiles and retain bounded cold-run diagnostics

* Document independent native proofs and preserve source anchors

docs/ci.md replaces two superseded descriptions of the old shared
native-live job:

- dbd17b3 (#320): the sentence "native-live.yml runs both profiles
  alongside native, reusing the same checksum-pinned WebSocket-enabled
  curl" is replaced by the three independent matrix cells description.
- INT26 (d093560): the old `native-live` timeout-table row (the larger
  runner's full set "about 25 min", "The verification hosts' release
  profile, the nearest they run, took 11.2-18.4 min" and "A fork's pull
  request runs the set without that row: 3.4 min on the standard
  runner") is replaced by the per-cell native, live-sanitizers and
  live-tsan rows measured on main runs 37398893123, 37461653250 and
  37478400505.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Bound native rows by the slowest measured host and keep failed-row counts

A cold full-row native profile of d5ffd30 on a c5.4xlarge Spot host ran
941/941 rows: 940 passed and test_ci_verify was cut off by the 2100 s
default deadline (Timeout 2100.07 s). The same script then completed in
2177.7 s (162 tests OK) in that host's preflight. GitHub measured
984-1695 s. Raise the native default per-test deadline to 2700 s (24%
over the slowest measured host) inside a 2820 s CTest stage. The measured
cold pre-CTest time was 12.3 min, so a worst-case stage still fits the
unchanged 65-min verification step and 75-min job. Live profiles keep
900/1200 s.

That failed run's ci-summary recorded no executed/skipped counts. CTest
prints a listed row's labels after its status ("2 - test_ci_verify
(Timeout)   slow"), and CTEST_LISTED_ROW required the line to end at the
status, so the row list was unreadable. Accept trailing labels. A new
real-CTest test pins labelled failed, skipped and not-run rows.

docs/ci.md rewords the `native-live (native)` timeout-table row written
by the docs commit "Document independent native proofs and preserve
source anchors". Its sentence "The longest measured test_ci_verify row
was 1695 s; its new 2100-s default deadline gives 24% margin within a
2400-s CTest stage" now carries the Spot measurement and the new bounds.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* Keep real CTest deadline test compatible with optional JUnit

Use the existing CTest capability detector and condition only XML presence; retain the real timeout, witness, row-count, elapsed and log checks.

Rewrite the driver-default and blanket two-times timeout sentences introduced by d093560 (INT26, #290). Document the workflow's 47-minute native and 20-minute live overrides and the table's measured-margin policy without changing historical timings.

* Accept older CTest failed-list rendering without losing label coverage

Allow the real labelled fixture to render an undecorated failed-list row. Verify slow/live labels for every fixture row through genuine CTest JSON inventory, retain the synthetic label-suffixed parser witness and all row accounting, and require the real failure exit code. Leave JUnit capability handling and production code unchanged.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
luisleo526 added a commit that referenced this pull request Oct 25, 2026
…#481)

* test(ci): pin equivalence CTest row and prerequisite contract (CR-223-ROW)

* test(ci): require equivalence execution and corpus LFS provisioning

* test(ci): extend verifier partition and native row accounting

* ci(live): run corpus equivalence as a required CTest row (CR-223-ROW)

The corpus equivalence driver becomes a permanent runner CTest row, and the
runner-e2e equivalence exclusion leaves `ci_verify.py`.

Two sentences of `docs/ci.md` that dbd17b3 ("Live runner: normalized feed input
and per-action webhook routing (#320)") introduced are retired here because this
change makes them false, and the message names them so the doc-revert gate can
read the retirement:

- "The standalone corpus equivalence driver is not a runner CTest row; its
  harness unit tests remain in the engine-wide inventory." The driver is now the
  `native_live_equivalence_e2e` row in `runner/CMakeLists.txt`, and the same
  sentence's second clause (the harness unit tests staying in the engine-wide
  inventory) is kept verbatim in the replacement text.
- "It refuses label exclusions, missing runner compile commands, missing
  ASan/UBSan or frame-pointer flags, skipped/disabled tests, and, while
  `runner/transport.cpp` exists, a missing WebSocket row or transport skip even
  without `--require-websocket`." Its "skipped/disabled tests" clause is rewritten
  because a required runner e2e skip now fails outright; every other clause is
  kept in the replacement paragraph.

Restored on purpose: nothing. The rest of the diff only adds text.

* ci: release ruleset probe lists every branch ruleset (measurement only, gate unchanged) [replaces #489] (#505)

* ci(release): the ruleset probe lists every branch ruleset as visible or hidden, even when the gate verdict is a refusal (CR-245 F1 measurement)

The one dispatch of release-ruleset-probe.yml from main (run 40083495967) refused on the first branch ruleset
of the inventory, the organization ruleset `default` (13094758): "returned missing or invalid bypass_actors
(token cannot see this ruleset's bypass_actors)". The verdict reader stops at the first failure, so the run
did not say whether the release App token can see bypass_actors on the repository ruleset that is the real
barrier (21476386), nor on any other. The probe now first prints one line per branch ruleset of the
includes_parents inventory (id, name, source_type, source, enforcement, updated_at, bypass_actors visible
with the actor count or hidden, applies to main, has a pull_request rule), then runs the unchanged verdict:
success prints the existing success lines; a refusal prints the same ::error:: text on stderr and exits 1,
but stdout now carries the listing. A detail read that fails is reported on its own row and the listing
continues. The listing is a measurement: `gate`, `_release_bump` and `_require_release_rulesets` are
untouched, every gate test and the existing source mutants are kept, and the probe's verdict replay over every
existing ruleset fixture still compares verdict and refusal text byte for byte. The workflow
release-ruleset-probe.yml is not changed. The listing reads each branch ruleset detail independently of the
verdict read (two reads per detail in the probe), which docs/ci.md now says.

Red-first rows: the real situation (org ruleset hidden, repository ruleset visible: both listed, the org one
hidden, refusal unchanged), both hidden, one detail read failing, all visible with a valid barrier, the
read-only call list, and a source mutant that drops the listing.

Rewrites named for the doc-reverts check, all introduced by 9c18d41 (#488, "ci(release): read-only ruleset
probe for the release gate (CR-245 F1)"): docs/ci.md "Success prints the barrier id/name and one compact
inventory line with enforcement, source and bypass visibility."; docs/ci.md "Refusal exits 1 with empty stdout
and the reader's `::error::` diagnostic; API/response failures name the endpoint."; docs/ci.md "Two hidden
rulesets need two dispatches: the first refusal names only the first hidden ruleset; make its `bypass_actors`
visible before dispatching again to identify the second." (replaced: all hidden rulesets are listed in one
dispatch); the probe docstring "Read exactly the bump's ruleset evidence, with the same refusal policy."; two
test comments ("The reader report's complete [] inventory fails on main without an endpoint." and "The
runtime assertions already hold there; the prose does not."). The module docstring sentence "Each
endpoint/page is read at most once; failures refuse without retry or sleep." (ad1ebac, #479) is scoped to the
gate's reads.

Lane: ci-fu-cr488m (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci(release): the probe listing bounds its inventory=ERROR row to 240 characters like the detail-error rows (CR-245 F1 measurement, sol P2)

The new stdout listing printed the whole exception text on an `inventory=ERROR` row, while the per-ruleset
`detail=ERROR` rows truncate it to 240 characters; a `gh` transport error with 300 characters of stderr was
echoed in full. The row now applies the same bound ([:240], still !r-escaped). Four rows assert the exact bounded
line (an inventory transport error, a repository transport error, a default-branch rejection with a long repository
argument, an invalid summary with a long field) together with the unchanged stderr, exit code 1 and API call list
of the verdict; each has a mutation control that removes only the bound and must fail. The verdict functions are
untouched and the verdict's own stderr is not bounded. No documentation sentence is changed (none described a length
limit).

Lane: ci-fu-cr488n (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>

* ci: fully qualify the two remote-tracking refs the promote-baseline ancestry check and the doc-reverts default base read (ORG-ACTIONPINS-CARD) [replaces #494] (#507)

* ci: fully qualify the two remote-tracking refs the promote-baseline ancestry check and the doc-reverts default base read (ORG-ACTIONPINS-CARD, LB-464 P3)

promote-baseline.yml tested `git merge-base --is-ancestor "$MERGE_SHA" "origin/$BASE_REF"`, and
check_doc_reverts.py default_base() resolved the bare names `origin/main` then `main`. A short name resolves through
git's ref-lookup order, so a tag called `origin/main` (or `main`) would win over the remote-tracking branch or the
local branch. Both now use the fully qualified names (`refs/remotes/origin/$BASE_REF`; `refs/remotes/origin/main`,
then `refs/heads/main`, same order and same fallback to the merged-pull-request walk). The exact-text pin in
scripts/ci_preflight.py is updated to the new command and stays as strict; the old short form is now a finding.

Red-first tests: the pin refuses the short command and accepts the qualified one; a repository where a TAG named
`origin/main` points at an unrelated commit and the real remote-tracking ref points elsewhere, and the same for a tag
named `main` with no remote-tracking ref, each returns the merge base of the real ref and not the tag's. Five controls
keep the order, the environment override, the local fallback, the merged-pull-request fallback and the no-base result.
The first-parent walk is untouched (the lab-host defect is a separate card).

The default_base docstring in scripts/check_doc_reverts.py is re-wrapped around the two renamed refs; the sentences it
carries come from 1e360bd and d093560 and keep their wording, only the ref names change.

Lane: ci-fu-shortrefs (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci: release ruleset probe lists every branch ruleset (measurement only, gate unchanged) [replaces #489] (#505)

* ci(release): the ruleset probe lists every branch ruleset as visible or hidden, even when the gate verdict is a refusal (CR-245 F1 measurement)

The one dispatch of release-ruleset-probe.yml from main (run 40083495967) refused on the first branch ruleset
of the inventory, the organization ruleset `default` (13094758): "returned missing or invalid bypass_actors
(token cannot see this ruleset's bypass_actors)". The verdict reader stops at the first failure, so the run
did not say whether the release App token can see bypass_actors on the repository ruleset that is the real
barrier (21476386), nor on any other. The probe now first prints one line per branch ruleset of the
includes_parents inventory (id, name, source_type, source, enforcement, updated_at, bypass_actors visible
with the actor count or hidden, applies to main, has a pull_request rule), then runs the unchanged verdict:
success prints the existing success lines; a refusal prints the same ::error:: text on stderr and exits 1,
but stdout now carries the listing. A detail read that fails is reported on its own row and the listing
continues. The listing is a measurement: `gate`, `_release_bump` and `_require_release_rulesets` are
untouched, every gate test and the existing source mutants are kept, and the probe's verdict replay over every
existing ruleset fixture still compares verdict and refusal text byte for byte. The workflow
release-ruleset-probe.yml is not changed. The listing reads each branch ruleset detail independently of the
verdict read (two reads per detail in the probe), which docs/ci.md now says.

Red-first rows: the real situation (org ruleset hidden, repository ruleset visible: both listed, the org one
hidden, refusal unchanged), both hidden, one detail read failing, all visible with a valid barrier, the
read-only call list, and a source mutant that drops the listing.

Rewrites named for the doc-reverts check, all introduced by 9c18d41 (#488, "ci(release): read-only ruleset
probe for the release gate (CR-245 F1)"): docs/ci.md "Success prints the barrier id/name and one compact
inventory line with enforcement, source and bypass visibility."; docs/ci.md "Refusal exits 1 with empty stdout
and the reader's `::error::` diagnostic; API/response failures name the endpoint."; docs/ci.md "Two hidden
rulesets need two dispatches: the first refusal names only the first hidden ruleset; make its `bypass_actors`
visible before dispatching again to identify the second." (replaced: all hidden rulesets are listed in one
dispatch); the probe docstring "Read exactly the bump's ruleset evidence, with the same refusal policy."; two
test comments ("The reader report's complete [] inventory fails on main without an endpoint." and "The
runtime assertions already hold there; the prose does not."). The module docstring sentence "Each
endpoint/page is read at most once; failures refuse without retry or sleep." (ad1ebac, #479) is scoped to the
gate's reads.

Lane: ci-fu-cr488m (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci(release): the probe listing bounds its inventory=ERROR row to 240 characters like the detail-error rows (CR-245 F1 measurement, sol P2)

The new stdout listing printed the whole exception text on an `inventory=ERROR` row, while the per-ruleset
`detail=ERROR` rows truncate it to 240 characters; a `gh` transport error with 300 characters of stderr was
echoed in full. The row now applies the same bound ([:240], still !r-escaped). Four rows assert the exact bounded
line (an inventory transport error, a repository transport error, a default-branch rejection with a long repository
argument, an invalid summary with a long field) together with the unchanged stderr, exit code 1 and API call list
of the verdict; each has a mutation control that removes only the bound and must fail. The verdict functions are
untouched and the verdict's own stderr is not bounded. No documentation sentence is changed (none described a length
limit).

Lane: ci-fu-cr488n (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: ci-cx <ci-cx@invalid>
Co-authored-by: Claude Sonnet 5.5 <[email protected]>

* ci(release): the permission-claim text guard also catches backslash-wrapped, bytes-decoded and fullwidth respellings (PROBE-FU) [replaces #493] (#506)

* ci(release): the permission-claim text guard also catches backslash-wrapped, bytes-decoded and fullwidth respellings (PROBE-FU, sol P2 1/2/4)

The guard in WorkflowPinTests.assert_ruleset_visibility_documentation fails when the gate source, docs/ci.md or
release.yml state that the App needs "administration: read". A read of the probe found three respellings it accepted:
a Markdown hard break written as a backslash before the newline, a bytes constant (b"administr\x61tion: re\x61d",
with or without .decode()), and fullwidth compatibility letters. The collapsed view now treats backslash-newline like
whitespace, bytes constants are decoded into the scanned strings, and every view is NFKC-normalised before case
folding. Three new tests (12 rows) each read the real artifacts, assert the unmodified baseline passes, mutate a copy
and require the specific permission-claim AssertionError; removing only the matching handling turns its rows red.

Not closed, by design: YAML double-quoted scalars with escapes in release.yml (sol P2 3) and runtime string
construction (sol P3); a static text guard cannot decide them, and the exact-output pins on the refusal text remain the
decisive protection. No existing assertion, control or forbidden pattern is changed. The rewritten `view` tuple is code,
not a documentation or comment sentence.

Lane: ci-fu-pfuguard (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci(release): the permission-claim guard scans the casefold-only views as well as the NFKC views (PROBE-FU round 2, sol R237 P2)

The first PROBE-FU commit normalised every scanned view with NFKC and replaced the casefold-only views. For an input
such as `Needs administration: read` followed by the trade mark sign, the old guard matched (the sign is a non-word
character after `read`), but after NFKC the text is `readtm` and the word boundary no longer matches, so the claim was
accepted: the change made the guard weaker for that input. Each text is now checked in four views (the two original
views casefolded, and both NFKC+casefolded), so normalisation can only add matches. A restoration control appends the
trade mark case to the gate source, docs/ci.md and the workflow text (direct and line-wrapped, six rows), and a removal
mutant drops the casefold-only scan and must break all six rows.

No existing assertion or control changes; no documentation or comment sentence is deleted or rewritten.

Lane: ci-fu-pfuguard2 (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci: release ruleset probe lists every branch ruleset (measurement only, gate unchanged) [replaces #489] (#505)

* ci(release): the ruleset probe lists every branch ruleset as visible or hidden, even when the gate verdict is a refusal (CR-245 F1 measurement)

The one dispatch of release-ruleset-probe.yml from main (run 40083495967) refused on the first branch ruleset
of the inventory, the organization ruleset `default` (13094758): "returned missing or invalid bypass_actors
(token cannot see this ruleset's bypass_actors)". The verdict reader stops at the first failure, so the run
did not say whether the release App token can see bypass_actors on the repository ruleset that is the real
barrier (21476386), nor on any other. The probe now first prints one line per branch ruleset of the
includes_parents inventory (id, name, source_type, source, enforcement, updated_at, bypass_actors visible
with the actor count or hidden, applies to main, has a pull_request rule), then runs the unchanged verdict:
success prints the existing success lines; a refusal prints the same ::error:: text on stderr and exits 1,
but stdout now carries the listing. A detail read that fails is reported on its own row and the listing
continues. The listing is a measurement: `gate`, `_release_bump` and `_require_release_rulesets` are
untouched, every gate test and the existing source mutants are kept, and the probe's verdict replay over every
existing ruleset fixture still compares verdict and refusal text byte for byte. The workflow
release-ruleset-probe.yml is not changed. The listing reads each branch ruleset detail independently of the
verdict read (two reads per detail in the probe), which docs/ci.md now says.

Red-first rows: the real situation (org ruleset hidden, repository ruleset visible: both listed, the org one
hidden, refusal unchanged), both hidden, one detail read failing, all visible with a valid barrier, the
read-only call list, and a source mutant that drops the listing.

Rewrites named for the doc-reverts check, all introduced by 9c18d41 (#488, "ci(release): read-only ruleset
probe for the release gate (CR-245 F1)"): docs/ci.md "Success prints the barrier id/name and one compact
inventory line with enforcement, source and bypass visibility."; docs/ci.md "Refusal exits 1 with empty stdout
and the reader's `::error::` diagnostic; API/response failures name the endpoint."; docs/ci.md "Two hidden
rulesets need two dispatches: the first refusal names only the first hidden ruleset; make its `bypass_actors`
visible before dispatching again to identify the second." (replaced: all hidden rulesets are listed in one
dispatch); the probe docstring "Read exactly the bump's ruleset evidence, with the same refusal policy."; two
test comments ("The reader report's complete [] inventory fails on main without an endpoint." and "The
runtime assertions already hold there; the prose does not."). The module docstring sentence "Each
endpoint/page is read at most once; failures refuse without retry or sleep." (ad1ebac, #479) is scoped to the
gate's reads.

Lane: ci-fu-cr488m (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci(release): the probe listing bounds its inventory=ERROR row to 240 characters like the detail-error rows (CR-245 F1 measurement, sol P2)

The new stdout listing printed the whole exception text on an `inventory=ERROR` row, while the per-ruleset
`detail=ERROR` rows truncate it to 240 characters; a `gh` transport error with 300 characters of stderr was
echoed in full. The row now applies the same bound ([:240], still !r-escaped). Four rows assert the exact bounded
line (an inventory transport error, a repository transport error, a default-branch rejection with a long repository
argument, an invalid summary with a long field) together with the unchanged stderr, exit code 1 and API call list
of the verdict; each has a mutation control that removes only the bound and must fail. The verdict functions are
untouched and the verdict's own stderr is not bounded. No documentation sentence is changed (none described a length
limit).

Lane: ci-fu-cr488n (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: ci-cx <ci-cx@invalid>
Co-authored-by: Claude Sonnet 5.5 <[email protected]>

* tests: drop the TIMEOUT 180 ctest property (owner's no-time-limit rule, P2-CM-F4) (#504)

* tests: drop the TIMEOUT 180 property from the six ownership manifest tests (P2-CM-F4)

One set_tests_properties call carried TIMEOUT 180 for test_selected_window_ownership_manifest,
the four test_selected_window_ownership_manifest_controls_shard_{1..4}of4 and
test_selected_window_ownership_d4. The owner's no-time-limit rule forbids a ctest
ceiling: under pool contention shard 1of4 (119 s normally) hit the 180 s ceiling on the
b250ceb merge-commit ci-post (#283). WORKING_DIRECTORY is kept; no test, label or
line count changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci: release ruleset probe lists every branch ruleset (measurement only, gate unchanged) [replaces #489] (#505)

* ci(release): the ruleset probe lists every branch ruleset as visible or hidden, even when the gate verdict is a refusal (CR-245 F1 measurement)

The one dispatch of release-ruleset-probe.yml from main (run 40083495967) refused on the first branch ruleset
of the inventory, the organization ruleset `default` (13094758): "returned missing or invalid bypass_actors
(token cannot see this ruleset's bypass_actors)". The verdict reader stops at the first failure, so the run
did not say whether the release App token can see bypass_actors on the repository ruleset that is the real
barrier (21476386), nor on any other. The probe now first prints one line per branch ruleset of the
includes_parents inventory (id, name, source_type, source, enforcement, updated_at, bypass_actors visible
with the actor count or hidden, applies to main, has a pull_request rule), then runs the unchanged verdict:
success prints the existing success lines; a refusal prints the same ::error:: text on stderr and exits 1,
but stdout now carries the listing. A detail read that fails is reported on its own row and the listing
continues. The listing is a measurement: `gate`, `_release_bump` and `_require_release_rulesets` are
untouched, every gate test and the existing source mutants are kept, and the probe's verdict replay over every
existing ruleset fixture still compares verdict and refusal text byte for byte. The workflow
release-ruleset-probe.yml is not changed. The listing reads each branch ruleset detail independently of the
verdict read (two reads per detail in the probe), which docs/ci.md now says.

Red-first rows: the real situation (org ruleset hidden, repository ruleset visible: both listed, the org one
hidden, refusal unchanged), both hidden, one detail read failing, all visible with a valid barrier, the
read-only call list, and a source mutant that drops the listing.

Rewrites named for the doc-reverts check, all introduced by 9c18d41 (#488, "ci(release): read-only ruleset
probe for the release gate (CR-245 F1)"): docs/ci.md "Success prints the barrier id/name and one compact
inventory line with enforcement, source and bypass visibility."; docs/ci.md "Refusal exits 1 with empty stdout
and the reader's `::error::` diagnostic; API/response failures name the endpoint."; docs/ci.md "Two hidden
rulesets need two dispatches: the first refusal names only the first hidden ruleset; make its `bypass_actors`
visible before dispatching again to identify the second." (replaced: all hidden rulesets are listed in one
dispatch); the probe docstring "Read exactly the bump's ruleset evidence, with the same refusal policy."; two
test comments ("The reader report's complete [] inventory fails on main without an endpoint." and "The
runtime assertions already hold there; the prose does not."). The module docstring sentence "Each
endpoint/page is read at most once; failures refuse without retry or sleep." (ad1ebac, #479) is scoped to the
gate's reads.

Lane: ci-fu-cr488m (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci(release): the probe listing bounds its inventory=ERROR row to 240 characters like the detail-error rows (CR-245 F1 measurement, sol P2)

The new stdout listing printed the whole exception text on an `inventory=ERROR` row, while the per-ruleset
`detail=ERROR` rows truncate it to 240 characters; a `gh` transport error with 300 characters of stderr was
echoed in full. The row now applies the same bound ([:240], still !r-escaped). Four rows assert the exact bounded
line (an inventory transport error, a repository transport error, a default-branch rejection with a long repository
argument, an invalid summary with a long field) together with the unchanged stderr, exit code 1 and API call list
of the verdict; each has a mutation control that removes only the bound and must fail. The verdict functions are
untouched and the verdict's own stderr is not bounded. No documentation sentence is changed (none described a length
limit).

Lane: ci-fu-cr488n (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>

* ci: pin every third-party action to a full commit SHA with a version comment, and lint it in preflight (ORG-ACTIONPINS) [replaces #495] (#508)

* ci: pin every third-party action in the engine workflows to a full commit SHA with a version comment (ORG-ACTIONPINS)

66 `uses:` lines changed in .github/workflows/*.yml and .github/actions/setup-live-curl/action.yml: 65 mutable tags
rewritten to `@<40-hex sha> # v<version>` and one bare SHA that lacked its comment. Each pin is the same major the file
used before; no step, job, permission, environment, secret or token mint changes (a before/after comparison of the
diff with the refs stripped is identical line for line). Resolved on 2026-10-24 22:48 with `git ls-remote --tags` on a
spot box; every tag is lightweight, so the tag's sha is the commit:

  actions/checkout v4                         11d5960a326750d5838078e36cf38b85af677262  v4.4.0
  actions/upload-artifact v4                  ea165f8d65b6e75b540449e92b4886f43607fa02  v4.6.2
  actions/setup-python v5                     a26af69be951a213d495a4c3e4e4022e16d87065  v5.6.0
  actions/cache, cache/save, cache/restore v4 0057852bfaa89a56745cba8c7296529d2fc39830  v4.3.0
  actions/create-github-app-token v1          d72941d797fd3113feb6b93fd0dec494b13a2547  v1.12.0
  cloudflare/wrangler-action v3               9acf94ace14e7dc412b076f2c5c20b8ce93c79cd  v3.15.0

The byte-pinned copies of the workflow text that scripts/ci_preflight.py, scripts/test_ci_preflight.py and
scripts/test_ci_release_gate.py compare against (the embedded probe workflow, the release and cache expectations,
the Wrangler deployment, test mutations and fixtures) are moved to the same pinned text so each still equals its
workflow. Two tests read the real workflows and composite action and require a 40-hex sha and a version comment on
every third-party reference.

No documentation or comment sentence is deleted or rewritten.

Lane: ci-fu-pins (Codex gpt-6.1-sol).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* ci(preflight): refuse any third-party `uses:` that is not a 40-hex commit SHA with a same-line version comment; the lint accepts plain block-style YAML only (ORG-ACTIONPINS)

scripts/ci_preflight.py gains `action_pin_findings`: every `uses:` in .github/workflows/*.yml and every composite
.github/actions/**/action.yml (or .yaml) that is not a local `./` path or a `docker://` image must be
`owner/action[/path]@<40 lowercase hex> # v<version>` on the same line; quoted values, step and job-level `uses:`, remote
reusable workflows and a ref-less `uses:` are judged; the finding names file, line and the offending reference. The lint
is a regression guard for our own reviewed workflow files, so it does not parse arbitrary YAML: it accepts plain block-style
YAML and refuses, with a finding naming file, line, construct and source text, what it cannot follow: anchors, aliases, merge
keys, explicit keys, tags, non-empty flow mappings, quoted keys, backslash escapes in quoted values, quoted values that do not
end on their line, tab indentation, document markers, `uses` without a same-line scalar, unrecognised block lines, non-ASCII
whitespace in structural positions, and any non-LF line break (bare CR, NEL, LS, PS, VT, FF) or C0 control character in the
whole file. Allowed (surveyed on the real tree, twelve YAML files): plain block mappings and sequences, plain and quoted
scalar values, empty `{}` and `[]` as a whole plain value, non-empty flow sequences, block-scalar bodies including `- |`.
Quotes inside flow sequences count only at element start; delimiters of a refused flow collection are counted without quote
masking and the refusal region ends at the first outdent. The `${{ ... }}` scan is linear. The loaders read the bytes and
decode strict UTF-8 (a file that is not UTF-8 is a finding), so a bare CR is no longer erased before the refusal runs. The
documented limit for exotic YAML is in docs/ci.md. The lint was read by sol and argon over five rounds (every finding closed
or classified); the loader and the three promotion-guard tests that now also see the lint's quoted-key finding were fixed
from the lab legs.

Tests: 15 first-round and the later red-first tests (one per refused construct, each break character in a block-scalar body and
in a comment, the C0 controls, flow-quote variants, outdent resumption, loader tests on real files in a temp layout, a real-tree
zero-findings test, mutation controls for the refusal branch and the integration call). docs/ci.md describes the rule.

This commit is the squash of the lint rounds of the series; the pins commit before it rewrites the workflow files. Lanes:
ci-fu-pinlint, ci-fu-pins2/3/4/5 (Codex gpt-6.1-sol), ci-fu-pins6 and pins7 (Claude Sonnet 5.5 via claudock).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: ci-cx <ci-cx@invalid>
Co-authored-by: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
Co-authored-by: ci-cx <ci-cx@invalid>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant