Skip to content

Lore CLI 0.12.0

Choose a tag to compare

@jeremy-newhouse jeremy-newhouse released this 30 Sep 18:11
· 240 commits to dev since this release
739f2b4

Added

  • lore query --across-refs — read the bundle across refs, read-only (LCLI-652; ODOC-330 /
    DEC-40, paired with quest-cli QCLI-417). A document added on an open pull request is invisible
    until it merges, because a checkout sees only its own bundle; this answers for the repository.
    Bare, the population is origin/dev plus every open pull request into dev, discovered
    through gh; given one or more ref names, it is exactly those refs, resolved locally with no
    forge and no fetch. Every hit gains refProvenance {ref, pullRequest, sha} — a separate key
    from --workspace's provenance, which carries an unrelated workspace identity — and every
    answer reports coverage {complete, population, discoveredAt, refsRead[], refsUnreadable[]}
    as an envelope key between data and principal, in the shape agreed byte-for-byte with
    quest-cli. Conflicting copies of one id are shown side by side, never merged: an id on
    origin/dev always emits dev's row, another ref's copy emits a row only when its bytes differ,
    and an id absent from dev emits one row per ref carrying it. Nothing is written — other refs
    are read with a destination-less git fetch (objects only) and git archive into a temporary
    directory, and lore sync refuses the flag, since a branch's managed blocks regenerate only
    from that branch's own records. An incomplete read is a drift error at exit 6 naming the
    unreadable refs and carrying the coverage object in the error's input, with nothing on
    stdout; --allow-partial answers at exit 0 with complete: false instead. With no gh on
    PATH, or an origin that is not GitHub, the run degrades to population: "dev-only" the same
    way — incomplete coverage, never an uncaught failure.

Changed (breaking)

  • The pinned Bun moved from 1.3.14 to 1.4.2 (LCLI-648, pair with quest-cli QCLI-411).
    Marked breaking because engines.bun moves with it to >=1.4.2, so the declared support floor
    narrows: a consumer running the package from source on Bun 1.3.x is now outside it. Nothing
    else about the surface changes.
    1.3.14 mis-parses a TS contextual keyword — declare, type, abstract, namespace,
    module, global, interface — that starts a larger expression, desynchronising the parser's
    scope tracking and panicking the runtime: oven-sh/bun#31239's repro exits 133 with no output
    on 1.3.14 and exits 0 on 1.4.2, and test/bun-declare-binding.test.ts pins that on the
    pinned runtime. Two upstream behaviour changes ride along. Bun.Glob now descends
    into dot-directories by default — .github/workflows/* returns 0 matches on 1.3.14 and 4 on
    1.4.2 (dot: true returned 4 on both) — which changes what a dotted lore replace --in
    pattern matches, previously nothing. And Bun.TOML.parse rejects an imprecise bare integer
    literal that 1.3.14 silently rounded; both runtimes still surface a stable validation error
    naming the config file, and test/config.test.ts covers each shape.

  • lore now refuses to drive a quest that is not its exact pair version (LCLI-650; DEC-31
    adopting the opum-doc ADR lock-lore-and-quest-to-their-exact-pair-version-at-runtime). The two
    CLIs carry one version number in every release (constitution Article 3 clause 1), so a correct
    install never notices: the check compares the full version string once, at the capability gate
    every quest-backed command passes before its first quest spawn. A mismatch fails — exit 6,
    validation, with a message naming both installed versions and the side to upgrade — rather than
    warning. Marked breaking because a mixed pair that used to work (lore X against quest Y) now
    refuses until the older side is upgraded; that is the intended cost, and the message names it.

  • lore init detects a tracker, offers, and instructs — it never installs, initializes, or migrates
    one for you
    (LCLI-656; DEC-57, adopting ADR-0024). Every path that used to act on the operator's
    behalf now stops with the exact commands instead. A quest selection with no quest on PATH
    stops with the install pinned to lore's own exact version (npm install -g @opum-ai/quest@<lore's version>) plus quest init (dropped when the repository already has a
    Quest workspace) and the rerun; a backlog selection likewise names backlog.md and its floor.
    A repository holding a Backlog project is offered the migration commands rather than migrated
    from the wizard, and no TTY means the same stop rather than an assumed "no". Marked breaking
    because three previously-advisory cases are now stops: --tracker quest or --tracker backlog
    with the CLI missing or the project uninitialized used to warn and persist a selection (exit 0)
    and now stop with the instructions (exit 3/6), writing nothing at all — the stop precedes the
    scaffold, so neither the selection nor the bundle lands until the backend can serve it. --install-tracker and --no-install-tracker are accepted for one release as no-ops that
    print a deprecation note — they install nothing, on any path, and together remain a usage error.
    The wizard's post-migration backlog/ removal offer is gone with the wizard's migration arm
    (the flag path's --remove-backlog/--no-remove-backlog is unchanged), --no-tracker remains the
    documented opt-out, and a bare non-interactive lore init still pins the default backend and
    probes nothing (LORE-260). The remedy names lore's own exact quest version, never latest, so the
    onboarding message and the runtime pair-lock refusal are one sentence.

Changed

  • A profile anchor on a heading nested inside a blockquote or a list item resolves, instead of
    crashing lore agent context
    (LCLI-647; DEC-22 option A). The renderer read top-level headings
    only while the validator's walk saw every node, so such a profile validated and then exited 1
    with zero bytes of stdout; both now read one shared heading enumeration. A nested section's region
    stops at its container and its breadcrumb names its nested ancestors; an anchor that matches no
    heading is a validation refusal (exit 6) with a remedy, never an uncaught crash. Where a
    duplicate heading takes a slug (a quoted # Overview beside a top-level ## Overview), an anchor
    on that slug now resolves to the heading the slugger assigns it — the meaning the validator and
    GitHub's own anchors already used. That re-points one case a profile could have relied on: an
    anchor of #overview where a quoted # Overview took the slug resolves to the QUOTED heading,
    not the top-level one the pre-fix renderer picked; anchor #overview-1 for the top-level heading.
    Classified ### Changed rather than breaking: the pack item's target changes, but nothing is
    refused that worked before, and the pre-fix target was the violation of this repository's own
    retrieval spec.

  • A RESUMED promote-latest run now refuses when any package's current latest is newer than
    --version
    (LCLI-638, twin of quest-cli QCLI-405). The LCLI-631 gate validated the record a
    FRESH run was about to write, and a resume reuses the record the first run wrote instead — so
    nothing compared the registry as it reads NOW with --version, and a run resumed after something
    else moved latest ahead would move it backwards, the move the ADR refuses. planPromotion now
    makes the same numeric comparison on the resume path, before any write, naming the package and
    both versions; a package already at --version stays accepted, which is the partial state a
    resume exists for. The comparison is by the release a live value leads with, so a registry
    latest of 5.7.0-rc.1 counts as newer than 5.6.7 and refuses, while 5.6.7-rc.1 or
    5.6.7+build.7 is not newer than 5.6.7 and still resumes. On a fresh run, a package whose
    current latest is not a plain X.Y.Z now gets a headline in the registry's own terms before
    validateRecord's "recorded prior latest ...", which named a value nothing had recorded yet.

  • Every release script now runs its CLI when it is invoked through a symlinked path (LCLI-637,
    twin of quest-cli QCLI-404). Six scripts guard main() — launcher-equivalence, pair-receipt,
    promote-latest, version-parity, github-release and check-breaking-bump — and all six
    compared resolve(process.argv[1]) with fileURLToPath(import.meta.url). That is false through
    any symlinked path, because resolve() never follows a link while Node resolves import.meta.url
    to the module's real one; the macOS tmpdir (/var -> /private/var) trips it with no symlink of
    anyone's own. The failure was silent in the worst way: the process exited 0 having done nothing,
    so a release gate reported success for work it never did. All six now call one shared
    realpath-based isMain() in scripts/is-main.mjs, and test/lcli637-is-main-symlink.test.ts
    invokes each of them through a real symlink and fails against the old guard. Measured before:
    node scripts/version-parity.mjs --help exits 2 with 52 bytes of usage, the same file through a
    symlinked directory exits 0 with 0 bytes. After: both exit 2 with byte-identical output.
    check-breaking-bump.mjs is in the list because re-measuring the inventory repo-wide found it,
    not the four sites the record named.

  • provenance-post's not-published warning now leads with the cause that run's publish job
    allows
    (LCLI-635, LCLI-628's reviewer finding F4). A package this release should have published
    but the registry does not list has two explanations — a publish that stopped partway, or the
    registry read API lagging a publish that succeeded (LCLI-460). The warning led with the partial
    publish either way, so on a run whose publish job was green it named a cause that could not be
    true and prescribed "Re-run failed jobs" for bytes that were already on the registry. release.yml
    now passes needs.publish.result to scripts/release-provenance.mjs --post as the new
    --publish-result flag, which is required there: success means every package is on the registry,
    so the warning leads with the read-API lag and does not prescribe a resume, while any other value
    makes a partial publish the likely cause and sends the reader to the publish job's log, the one
    thing that tells the two apart. The same choice orders the job summary's verdict. A value outside
    GitHub's result vocabulary is refused (exit 2) rather than read as "not success", and
    --publish-result is refused on --pre, which has no not-published branch.

  • A malformed window variable now fails the release BEFORE the first publish, and fails a
    publish:false rehearsal too
    (LCLI-634, extending LCLI-630). vars.REGISTRY_WINDOW_SECONDS and
    vars.PROVENANCE_WAIT_SECONDS are refused unless they are a whole number of seconds (0 to
    999999999, no leading zero), naming the variable and showing the value escaped. A new always-run
    release-window job refuses the repository/organisation-scoped value on every dispatch; the
    publish job refuses the environment-resolved one before its first publish. The LCLI-630
    post-publish check stays as defence in depth. scripts/release-provenance.mjs's --wait-seconds
    now refuses 30m, 08, 1e3 and abc instead of reading them as 30, 8, 1 and a crash.

  • Release prep refuses a patch-level bump when the notes for the version being released carry a
    breaking-change marker
    (LCLI-632, paired with quest-cli QCLI-328; ADR in opum-doc).
    scripts/check-breaking-bump.mjs compares the checked section with the previous version heading
    and refuses a patch bump when that section carries a ### ... (breaking) heading, or lore's
    legacy bold behaviour-change marker, whose refusal names the canonical ### Changed (breaking)
    spelling. Because lore and quest share one version (constitution Article 3 clause 1), the gate
    also reads quest-cli's CHANGELOG.md by ref and applies the same rule, and an unreadable quest
    changelog is a refusal, not a pass. release.yml's new breaking-bump job gates the publish job.

  • scripts/release-provenance.mjs refuses an acknowledge_dangling_provenance dispatch input
    that could forge workflow commands
    (LCLI-636). The value previously printed raw inside
    ::warning:: lines, so an API dispatch with an interior CR/LF could have forged runner commands
    and annotations. The input is now refused (exit 2) when it still contains %, a newline, any
    other control character, or DEL after trimming, with the value shown escaped.

  • lore reads quest's terminal statuses from quest task status-flow instead of treating only
    Done as complete
    (LCLI-633, paired with quest-cli QCLI-331 and its later QCLI-406). A task in
    quest's closedStatus is now complete for Story/Task coupling and lore check, and the adapter
    accepts a terminal status set within the status flow plus closedStatus. A genuinely foreign
    terminal status is still drift, and its message names a lore/quest version mismatch rather than a
    too-old quest.

  • lore read renders the markdown body when stdout is a terminal (LCLI-615). It renders
    headings, emphasis, lists and task lists, block quotes, fenced code, links (as text (url)) and
    GFM tables, word-wrapped to the terminal's width. --plain, piped output and --json are
    unchanged and byte-for-byte, so lore read <id> | tail -n +3 still recovers the body. Terminal
    control sequences in a document are removed before rendering, including ones produced by
    character references such as &#x202E;. NO_COLOR removes all styling and keeps the layout.
    New exact-pinned build dependencies, compiled into the binary: mdast-util-gfm 3.1.0 and
    micromark-extension-gfm 3.0.0 (MIT; the binary grows by about 66 KB).

  • scripts/promote-latest.mjs cuts lore's GitHub Release itself, and refuses before anything
    moves if it could not
    (LCLI-622, paired with quest-cli QCLI-398 and QCLI-401). On --dry-run
    and --promote alike, before the record or any registry write, it refuses when CHANGELOG.md
    has no non-empty ## [X] section, when gh cannot read the repository, or when vX already
    exists as a draft, a prerelease or with different notes. An existing release is never edited.
    Once latest is verified it removes the private npmrc, then creates vX from that section
    (non-draft, non-prerelease, --verify-tag, marked latest), or only marks an identical existing
    release latest. A failed cut exits 3 with "do NOT roll back" and the repair command
    node scripts/github-release.mjs --version X --create, and the README read-back still runs. The
    post-latest checklist reports the outcome instead of printing gh release create. The first
    live cut is the next promotion.

  • scripts/promote-latest.mjs refuses a --version that would move latest backwards, or is
    not plain X.Y.Z
    (LCLI-631, paired with quest-cli QCLI-402). On a fresh --dry-run or
    --promote, before any registry write, it refuses a --version older (numerically) than any
    package's current latest, naming both versions, and it refuses when a current latest is not
    a plain release. Before this, such a run wrote a record that both resume and --rollback then
    refused, which stranded the promotion. A prerelease --version was already refused earlier, by
    the equivalence check, and a test now holds that end to end. Backports and prereleases are not
    a promote use case. A resumed run was not re-checked against a current latest until LCLI-638,
    which closed that: see the bullet above.

  • The root launcher stages as X-rc.N; the platform packages still stage as X (LCLI-621,
    constitution Article 3 clause 5 as amended by ODOC-302). The Release run's npm-packages
    artifact now carries eight tarballs: the six platform packages at X, the launcher at X-rc.N
    and the launcher at X. Staging, from release.yml's publish job or from
    scripts/publish-release.sh, publishes seven of them under release-candidate: the platforms
    at X and the launcher at X-rc.N, which pins the platforms at exactly X. The X launcher
    is carried and never staged. N is the new launcher_rc dispatch input, which defaults to 1.
    A re-stage of the same X takes the next N, and works only if the new run's platform tarballs
    are byte-identical to the ones already on the registry. That has been measured once: two Release
    dispatches on the same commit, 20a1d24b (0.6.1, runs 34783940117 and 34786808767), produced
    seven byte-identical tarballs. From a different commit it is unmeasured. The final X
    launcher reaches latest through scripts/promote-latest.mjs; see the next entry.

  • latest gets the launcher by a fresh publish of the qualified X, last (LCLI-621, Part B).
    scripts/promote-latest.mjs now takes --release-run <id>. It downloads that run's
    npm-packages artifact afresh and re-runs the equivalence gate on it. It then re-reads the
    pass-1 receipt against the artifact. That receipt's commit must be what v<version> peels to,
    its launcherVersion must be the artifact's X-rc.N, and its launcherSubstitution must be
    MATCH naming the basename opum-ai-lore-<X>.tgz at the artifact X launcher's sha256. The
    staging check reads X-rc.N on the launcher, and the pair receipt's launcherVersion is now
    required and must equal the artifact's rc. A receipt without launcherVersion refuses, as
    quest-cli's reader refuses it. Before anything moves, and again after the six platforms move by
    dist-tag, npm pack of the registry's X-rc.N must be sha256-identical to the artifact's rc.
    The X launcher must be equivalent to that served rc and still hash to the receipt's
    finalTarball.sha256. Then it runs npm publish <X> --tag latest, last. That is the one publish
    in the repository without --tag release-candidate, and a test holds it to that one site. If
    X is already on npm as the artifact's bytes, it moves the tag instead. Other bytes refuse
    before anything moves. Afterwards it verifies latest on all seven packages and npm's X
    integrity. It then reads the package-level readme back; the next entry says how (LCLI-616
    replaced a warn-only byte count). A failed platform move or launcher publish restores every
    latest that run moved, the launcher's included, by dist-tag. A failure in the step-7 checks
    afterwards restores nothing by itself, by design, and names --rollback <record> as the remedy.
    --rollback restores every recorded latest. Nothing is unpublished. Every npm call the promotion
    and pair-receipt scripts make passes both --registry=https://registry.npmjs.org/ and
    --@opum-ai:registry=https://registry.npmjs.org/. That covers the dist-tag moves, the publish,
    every npm view, and the npm pack of the served rc. --registry alone does not beat an
    @opum-ai:registry set in a user, project or global npmrc, as measured on npm 12.1.0. Every
    read also passes --userconfig=/dev/null, so no token is sent. The Release run must be a
    workflow_dispatch run built from opum-ai/lore-cli. The spec is quest-cli's promote clause
    (QCLI-399, e3c59d7b) and opum-cli-e2e's receipts/README.md steps 5 to 7 (4f078e6b).

  • The README read-back runs after the final latest publish, with three verdicts (LCLI-616,
    OPAG-474 AC3). scripts/promote-latest.mjs --promote runs scripts/readme-readback.sh once,
    after step 7, in a private directory holding the X launcher tarball's own package.json and
    README.md. The registry pins travel in its environment. Every inherited npm config variable,
    in either case, is dropped first, because an uppercase NPM_CONFIG_@OPUM-AI:REGISTRY beat the
    lowercase pin. REGISTRY_WINDOW_SECONDS passes through, and --dry-run does not run it. The
    script now ends every path, an interrupted one included, with one line:
    A4 VERDICT: PASSED|NOT-CONFIRMED|FAILED <reason>. Promote reads that line, not the last line
    printed. An empty readme after the window is settled by one packument read of readme and
    versions. If that read already lists the version, the verdict is FAILED (OPAG-474). If not, it
    is NOT-CONFIRMED, because lag is not ruled out: the packument had no readme before this
    publish. The previous-release pick skips X-rc.N versions, which sort just below X and never
    set the field. PASSED exits 0. NOT CONFIRMED and DID NOT PASS both exit 3, which since
    LCLI-622 also means the GitHub Release was not cut: a post-latest step did not complete. The message says the promotion is complete and verified, says not to run --rollback,
    and gives the commands that re-read the readme or re-run the read-back by hand. A temp
    directory, extraction or script that fails is a NOT CONFIRMED tooling failure, never an OPAG-474
    claim. release.yml's staging publish job no longer runs the read-back. A staged rc never sets
    the field, and the job's sparse checkout never carried the script, so every publish: true run
    concluded failure and could not be promoted. test/release-workflow.test.ts now requires every
    scripts/ file a job runs to be in that job's own checkout, and states what it cannot see.

  • The post-latest steps are printed after the promotion, not after staging (LCLI-618).
    scripts/publish-release.sh's closing checklist, from --print-checklist and at the end of a
    real run, now lists only what applies before the final latest publish. It tells opum-cli-e2e
    the staged pair is ready, with the Release run id, and gives the promote --dry-run and
    --promote commands. It then points at scripts/promote-latest.mjs for the rest. The README
    read-back, the GitHub Release, the "lore is live" notices, the LCLI-469 marketplace handshake
    and the release-truth record are gone from it. promote-latest.mjs --promote now prints them as
    a post-latest checklist when it finishes, whether or not the read-back passed. The checklist
    carries the read-back's state (PASSED, NOT CONFIRMED or DID NOT PASS) and verdict line, and
    the GitHub Release's outcome, which LCLI-622 made an executed step rather than a printed command. It lists the "lore is live" notices for quest-cli, opum-cli-e2e
    and opum-agent. It gives the four LCLI-469 handshake values, resolved: the tag object and
    peeled commit, and the skills/ tree SHA read through gh api. It also gives the run id and
    record path. It cites runbook section 3 item 8, which carries the
    same five steps, and a test holds the two together. publish-release.sh's post-publish
    registry-state table is now printed after the registry-visibility wait, not before it. On the
    0.11.0 staging it read the launcher as ABSENT through propagation lag, and the poll after it
    then found the launcher.

  • A resumed staging skips a package only if the registry holds this run's bytes (LCLI-621).
    Both release.yml's publish job and scripts/publish-release.sh compare an already-published
    package's dist.integrity with the run's tarball, platforms included, and refuse on a difference
    or an unreadable value. The comparison runs for all seven in a pre-flight before the first write,
    so a refusal leaves nothing published and never waits out the registry-visibility window. Before
    this, a platform package already on the registry was skipped unchecked, so an X-rc.N staged
    from a new Release run could install an older run's X platform bytes. Whether a package is
    already published is read as absent only from npm's own not-found (E404); any other npm view
    failure refuses, while the registry-visibility poll still retries it.

  • Staging now requires the receipt fields promotion requires (LCLI-621).
    scripts/publish-release.sh gates staging on scripts/pair-receipt.mjs --check-release-receipt.
    That is the same evaluateReleaseReceipt promotion runs, and it replaces a 60-line inline
    node -e checker that had never learned opum-cli-e2e's TASK-126 fields. A receipt must now name
    this run's X-rc.N as launcherVersion. It must also carry a launcherSubstitution of MATCH
    whose finalTarball names the basename opum-ai-lore-<X>.tgz at the carried launcher's sha256.
    Its tarballs must name exactly the seven staged packages. The carried X launcher is no longer
    accepted there even at its true digest, per opum-cli-e2e's receipts/README.md at e0021c7.
    Before this, staging accepted receipts that promotion then refused. The closing checklist's
    promote commands now pass --release-run <run-id>, without which promote-latest.mjs exits 2.

Changed (breaking)

  • lore check fails on every error-tier quote-safety finding that lore validate reports (LCLI-612, ADR-0007). It uses the same rule name and byte-identical messages,
    for every type. The findings cover an unquoted YAML-1.1 boolean (yes/no/on/off/y/n),
    a value that starts with a YAML indicator character, and a value containing : . Before this,
    such a file failed validate and passed check. Run lore validate to preview what check
    will now fail on. A bare YYYY-MM-DD date stays a validate-only warning. Frontmatter that is not
    valid YAML still stops check at the parse error, before per-file rules run. A fleet measurement
    by ref found 0 such findings in 396 files across 11 repositories. This ships in a minor release.

Added

  • lore check reports an agent profile whose declared sources cannot fit its max_tokens
    budget
    (LCLI-642, DEC-11). The new agent-profile-capacity finding is computed from the profile
    declaration alone — every candidate of every pinned/sources reference plus the auto-pinned
    Constitution, rendered by the same code the pack compiler uses, score annotations included — so it
    is the same on every task, needs no task string and compiles no pack. That distinction is the whole point of the check: a real
    pack omits declared sources on most tasks while being entirely within capacity, so a check built on
    a compiled pack would false-positive (measured on this repository's own documentation profile
    before the check was written). The finding names the profile, its budget, the declared set's size,
    and the sources that cannot fit, in declaration order. DEC-11 rules that it ships as a warning
    for one release and then as an error
    ; this release is the warning's, and the flip is one line —
    AGENT_PROFILE_CAPACITY_SEVERITY in src/core/check.ts — held by LCLI-646 and listed in the
    release runbook's "Held release steps" so the ordering is not left to memory. The check.report
    also carries agentProfileCounts (read, overCapacity, unmeasurable) and the printed report
    names it, so a clean capacity answer can never be a zero-input artifact. This repository's four
    profiles were re-budgeted to hold their complete declared sets — 108000 / 63000 / 94000 / 63000 —
    rather than narrowed, so no declared coverage was dropped; the budgets carry headroom for the task
    line and the query section, which the measurement itself does not count.

  • Launcher equivalence gate (LCLI-621). scripts/launcher-equivalence.mjs refuses an
    X-rc.N launcher that differs from the X launcher by anything but the version string. It
    compares entry by entry over the unpacked tarballs: the same set of paths, the same type and
    mode per entry, and byte-identical content after every X-rc.N is replaced with X. It also
    refuses an rc whose own package.json is not X-rc.N, and an rc pinning a platform at
    anything but X. It reads each archive to its end and refuses non-zero data after a zero
    block, because node-tar installs an entry placed after a lone zero block. It refuses any entry
    that is not a regular file or a directory, since a link's target is never compared (npm pack
    emits none). It runs in release.yml's package job and in scripts/publish-release.sh, and
    both launchers get the LCLI-510 shipped-README assertion and install-sanity.

Fixed

  • The GitHub Release's notes come from CHANGELOG.md at the commit v<version> peels to, not
    from the operator's working tree
    (LCLI-639, twin of quest-cli QCLI-407). releaseNotesFor read
    <checkout>/CHANGELOG.md out of the tree scripts/promote-latest.mjs ran in, so an uncommitted
    edit — or a section edited after the tag — became the body of a NEW release. An existing release
    was never at risk: differing notes are refused, and never edited. The bytes are now read with one
    pinned gh api raw-contents call at the peeled sha, for the pre-move refusal, the cut and the
    repair tool alike, addressed by the commit rather than the tag ref (a ref can be re-pointed, a
    commit cannot). releaseNotesFor is a pure function of the text it is given, and the repair
    command resolves the tag first: a tag whose commit carries no such section refuses naming that
    commit, and its remedies are a re-tag or a hand-cut, not an edit to a checkout the tag does not
    name. Measured on this repository: node scripts/github-release.mjs --version 0.9.0 now reports
    "would mark it latest" where the working-tree read reported differing notes. The extractor's fence
    handling is the rule agreed with quest-cli, refined after both reviews found the first version
    wrong in three ways: the fence state is computed for the whole document first and remembers the
    opener's character and run length (so a tilde line no longer closes a backtick fence, a
    four-marker fence no longer swallows the next entry, and a line of fewer markers no longer closes
    a longer one), a backtick fence's info string may not itself contain a backtick, and the section's
    START is subject to that state too, so a fenced ## [X] line can no longer hijack a section out
    of the real heading below it. An unclosed fence still runs to the end of the file. It implements
    the fence state and nothing else of CommonMark — no indented-code-block interaction, no lazy
    continuation, no container or list scoping.
  • provenance-post checks what a Release run published: the platforms at X and the launcher
    at X-rc.N
    (LCLI-625). Since LCLI-621 a Release run stages the launcher as X-rc.N, but
    scripts/release-provenance.mjs --post still checked every package at X. On a
    publish: true run it asked for @opum-ai/lore@X, which did not exist yet, and never checked
    the rc the run did publish. --post now requires --launcher-rc N. release.yml passes it
    from the launcher_rc input, and the script refuses a missing or malformed value with exit 2.
    The launcher's version is chosen by package name, so a --package override cannot route it
    back to X. --pre is unchanged, and refuses --launcher-rc. Each --post run says, in the
    log and the job summary, that the X launcher was not checked. That launcher is published
    later by scripts/promote-latest.mjs and carries no provenance. The release runbook now
    records it as "provenance-missing, byte-bound to the qualified rc". The equivalence gate and
    finalTarball.sha256 bind its bytes to the qualified rc, but they do not substitute for
    provenance. Restoring provenance on the final publish belongs to opum-agent's OPAG-127, for
    both CLIs.
  • provenance-pre re-verifies releases, not launcher version strings (LCLI-627). LCLI-625
    left scripts/release-provenance.mjs --pre unchanged. It still checked all seven packages at
    every version in the launcher's packument, which since LCLI-621 holds X-rc.N strings the
    platform packages never have. Against a packument of 0.7.0, 0.7.1 and 0.8.0-rc.1 to
    rc.3, --pre --limit 3 asked for 21 attestations, 18 of them for platform versions that can
    never exist. It never reached 0.7.1, never checked a platform at 0.8.0, and exited 0.
    --pre now groups the launcher's versions into releases by X, which is the version with only
    a trailing -rc.N removed. N follows the --launcher-rc rule. For each release it checks the
    platforms at X once, including a release whose launcher has only rcs, and the launcher at
    each version it has, once each. Every other version is its own release, checked at that exact
    string for every package. That includes a prerelease X such as 1.0.0-beta.1, which is
    separate from 1.0.0. It also includes a version that is not semver-shaped, which sorts last.
    --limit counts releases. A release at or below the baseline takes its rcs with it. Within a
    release, rcs run in numeric N order and X comes last. The defect was latent: no
    prerelease of @opum-ai/lore is on the registry yet. It had to be fixed before the first
    publish: true Release run under LCLI-621 puts one there.
  • scripts/publish-release.sh refuses a malformed registry window or propagation cushion before
    it reads anything remote
    (LCLI-629). Both REGISTRY_WINDOW_SECONDS and
    PROPAGATION_CUSHION_SECONDS were first read after all six platform packages were published,
    and neither was checked. Measured on the real script under macOS /bin/bash 3.2.57, a window of
    30m or 08 skipped the visibility gate and the cushion together, so the launcher was staged
    over a platform package that never became visible, and the run exited 0. A window of abc
    exited 0 after six platform publishes, with the launcher never published and no error. A cushion
    of abc was skipped, and 30m became a real 30-minute wait, because macOS sleep accepts unit
    suffixes. Both variables now have to be whole seconds, 0 to 999999999 with no leading zero. That
    is the grammar scripts/readme-readback.sh and scripts/promote-latest.mjs already use, and
    tests hold all three to one pattern. Anything else exits 2 before the version-parity read, any
    download, or any registry call, dry run included. The value is shown printf %q escaped on one
    line. --verify-only and --print-checklist read neither variable and are exempt. Unset or
    empty keeps the defaults, 1800 and 20.
  • provenance-post tells a never-published version from one published without an attestation
    (LCLI-628). npm's attestation endpoint answers a 404 in both cases. It was measured returning the
    same {"error":"Not found"} for @opum-ai/lore@0.11.0 and for the never-published
    9.9.9-rc.1. So a Release run whose publish job died before the launcher reported the rc
    launcher as unattested and blamed LCLI-482. It also spent the propagation window on a version
    that did not exist. --post still reads the attestation first, so a readable one is judged
    exactly as before. Only after a 404 does it ask whether the version exists. It believes a
    version-document 404 only when the packument also answers without that version, and reports
    that as not-published: its own verdict, with no propagation wait, and a warning that names a
    partial publish or read-API lag (LCLI-460). Any other answer to the existence read, such as an
    outage or a whole-package 404, is inconclusive, never not-published.
  • promote-latest --rollback refuses a recorded prior latest newer than the release
    (LCLI-617, paired with quest-cli QCLI-391). A hand-edited record whose priorLatest was newer
    than its version (5.7.0 in a 5.6.7 record) passed every earlier check. --rollback then
    moved latest onto a version no receipt qualified, because --rollback is not receipt-gated.
    Every priorLatest must now compare below record.version, each component compared by length
    and then lexically. That is exact numeric order past 2^53, so 0.10.0 is newer than 0.9.0.
    record.version itself must be a plain X.Y.Z string. The two repositories give identical
    verdicts, measured by each reading the other's rule by ref: 0 differences over 2583 inputs, with
    a positive control.
  • release.yml's post-publish visibility wait refuses a malformed REGISTRY_WINDOW_SECONDS
    before it reads anything
    (LCLI-630). The variable went into bash arithmetic unchecked. This was
    measured by running the step's own run: block as bash -e in an ubuntu:24.04 container,
    bash 5.2.21, which is not a GitHub runner. There, abc or an empty value silently made the
    window 0 seconds, so a lagging package got a green step and no content check. 30m or 08
    left the deadline empty, and against a lagging registry the step looped until the job timeout.
    An arithmetic subscript such as x[$(cmd)] ran cmd inside the publish job, which holds
    id-token: write. No such variable is set today at repository, release-environment or
    visible organisation level, so the hole has not been live. The step now refuses anything
    outside the shared REGISTRY_WINDOW grammar, whole seconds from 0 to 999999999 with no leading
    zero, with exit 2. It does this before any registry read, deadline or sleep. The value is shown
    %q- and %25-escaped on one ::error:: line. A test pins the pattern to
    REGISTRY_WINDOW.source. The guard still runs only after the publish; checking both window
    variables before the first publish is LCLI-634.