Repository navigation
GitPulse v1.2.0
A release about checks that were not telling the truth. Every fix here is a
report that read as an answer without being one: a doctor that compared a
version against itself and said ok for a binary running week-old code, an
installation check that named a cause it did not have, warnings dropped on the
way to the panel that promised to show them, and a measurement that silently
never happened on a window that had stopped painting. A check that could not
run must not read like one that ran and passed.
Repository trust is the other half. An approval made before 1.1.0 left every
linked worktree refused with nothing offering a way out — the app reported the
repository as trusted and never asked again, so worktree comparisons, collision
checks and the fleet view quietly dropped every sibling. Trust is now reported
as a scope rather than a yes/no, and the sidebar offers to extend an
approval that predates worktree coverage. Approvals are still never widened by
being read: extending is a decision you take, through the same dialog.
The new surface in this release, Firebase App Hosting, is built on the same
rule. Its backend and rollout listings are click-only rather than loaded on
render, because the upstream commands enable the App Hosting API on the project
they read — a read that bills is a decision, not a side effect of opening a
panel. What it buys is a join rather than a guess: App Hosting reports the full
commit SHA it deployed, so "this commit is live" is looked up against the graph
instead of inferred from a branch name and a timestamp.
That panel can now also deploy, and one action in this release changes what
production serves. Creating a rollout is neither reversible from GitPulse — App
Hosting publishes no rollback verb — nor idempotent, so it is the one place
here guarded twice rather than once: the target must be a full 40-character
SHA, the confirmation names the exact project, backend and commit and says
plainly that GitPulse cannot undo it, and editing any part of that target
disarms the confirmation so the values sent are always the values reviewed. The
policy gate runs before the process, not alongside it. And what the installed
CLI can actually do is probed rather than assumed, because the upstream
subcommand is registered only behind an experiment that is off by default — a
capability that could not be probed is reported as unknown, never as absent.
Added
-
App Hosting can deploy a named commit, not only report one.
Create rollout
is the only command in the Firebase module that changes what production
traffic serves, and it is built to be refused easily and taken deliberately.
The commit must be a full 40-character SHA — an abbreviation is an ambiguous
target for something that reaches production, and is declined with the
reason — and the deploying button does not exist until a confirmation naming
the project, the backend, the commit and the fact that GitPulse cannot undo
it has been read; changing any part of that target disarms it again. The
write gate judges the argv before any process is spawned, and a contract test
asserts that ordering directly rather than trusting it, because this argv is
built in the Firebase module instead of being spelled out in the handler and
is therefore exempt from the literal comparison the other commands get.
--tokenis never passed, so no credential is placed on a command line other
local processes can read, and--forceis never passed either, so nothing
here suppresses a prompt the user would otherwise have seen. A rollout that
started is never reported as failed: if the CLI exits zero without confirming
the result, the panel says so and warns against a blind retry, because
upstream allocates a new rollout id per call and a false failure is what
turns one deployment into two. -
The Firebase panel asks the installed CLI what it can do instead of assuming.
apphosting:rollouts:listis registered upstream only behind the
internaltestingexperiment, which is off by default, and an unregistered
subcommand exits non-zero having written nothing to either stream — which
would have read as "no rollouts" rather than as "this CLI cannot answer". The
probe lists a command group, so it needs no login, no project and no
network, and the action is offered only when the subcommand is really there.
A capability that could not be probed is reported as unknown rather than as
absent, because the two have different remedies. -
The board's quick-add line can draft. Return still saves the typed line
exactly as before, and then asks the configured model for a title and a
description to review. Two rules keep it honest, and neither is a tiebreak
applied afterwards: the task is written from the typed markers first and the
model proposes against the saved result, so markers always win structurally;
and the model can only ever touch title and description, because that is what
the enhancement field type admits — priority, labels, owner, type, repository
and due date are out of reach by type rather than by policy. The sentence
shown before you press Return and the request made after it are derived from
one value, so the promise on screen cannot name a field the request does not
carry. Off by default, and remembered: it changes what a keystroke does, so
it is chosen once rather than every time, and a stored value this build
cannot read falls back to manual — the mode that spends a model call must not
be the one a failed decode selects. -
Due is a control the sheet owns rather than the browser's
datetime-local
box. A trigger, a portaled month grid, and a word box that shares the board's
quick-add grammar — typefriday 09:30and press Return, the same phrasing
that works on the quick-add line, which is also what proves the grammar is
wired rather than duplicated.taskDueowns only what was unowned: the
grammar of a date word stays withparseQuickAddDueand how urgent a deadline
is stays withdueState, because the board sorts and filters on that and the
sheet must not answer it a second way. All of it is local time — a due date is
a day in the reader's week, not an instant, so the sheet cannot show "Sep 18"
for a timestamp the board files under the 19th. -
The plugin package has a display name, and a mark where a client will actually
render it.nameis the install identifier, so the list showedgitpulse.
The logo lives in.cursor-plugin/plugin.jsonand nowhere else: Cursor is the
only one of the three hosts that renders alogo, Claude Code documents no
field for one, and the Agent Plugins schema setsadditionalProperties: false,
so the same key in the portable manifest invalidates the package for a
conformant client.check-release-versionwalks the Cursor manifest with the
other three, so its version cannot drift from the release. -
Firebase App Hosting backends and rollouts, read through the
firebaseCLI
and joined to the commit graph.Build.source.codebasecarrieshash— a
full SHA-1, the same keygit cat-filespeaks — so "is this commit live?"
becomes a lookup rather than an inference, and presence is read from the
command's ownsuccessflag rather than fromis_ok(), because
git_capturedreturnsOkon a non-zero exit: absence is data, not failure,
and reading it the other way would have made every rollout look locally
present.The two listings are click-only and wear
Guarded<T>on purpose, and this is
the part worth stating plainly:apphosting:backends:listand
apphosting:rollouts:listboth declare.before(ensureApiEnabled)upstream,
so reading them enables the App Hosting API on the Cloud project. REST does
not avoid it — App Hosting's only OAuth scope iscloud-platform, full
read/write, with no read-only counterpart of the kind classic Hosting accepts.
A read that mutates a billed resource must be something you choose, never
something a panel does because it rendered. Passing--jsonalso implies
non-interactive, which turns a would-be enablement prompt into a loud error
rather than a silent one.apphosting:rollouts:listexists only in
firebase-tools' source and is documented nowhere, so its envelope is parsed
strictly and fixture-pinned instead of trusted to keep its shape.
Changed
-
Writing a task and asking the model to help with it are one pane again.
Splitting the sheet into four panes fixed a sheet where nothing read as
important and introduced a worse problem: the AI pane's output was drawn on
the Task pane, beside the fields each suggestion would replace, so a reader
pressed a button on one pane and the result appeared on another. Writing a
task, scheduling it and improving its wording are one sitting, so they are now
one pane in two columns. Only Agent still earns a tab — handing a saved
revision to something that will act on it is a different decision, with its
own risk and its own run history. A draft is still never tabbed. -
The suggestion picker can tell two attempts apart. Its rows read
state · modelplusTask revision N, which was the same string for two
attempts by the same model at the same revision. A timestamp separates
attempts made minutes apart and an ordinal separates attempts made in the
same second; the ordinal is a reading aid derived from the page total, never
an identity — the row's value is still the proposal id. State wording now
comes from one table read by both the picker row and the heading it selects,
which were previously the same literal list written twice. -
extended_child_pathleaves aPATHalready at theexecveargument-size
ceiling alone instead of appending to it. Growing a near-limit environment
turns a working spawn intoE2BIG, surfacing as "Failed to spawn git:
Argument list too long" with nothing naming the cause. -
The vendored DevCouncil crates move to
b366f42, taking the devmap store's
schema from 20 to 22. A GitPulse built from the previous vendored tree
refused the store its own repository already had — "schema version 22 is not
supported by this binary (schema 20)" — and the code graph then answered
"nothing" to every question, which reads exactly like a repository with no
callers.dc-procjoins the vendored closure at the same time: it had never
been vendored, butdc-verifyanddevmap-extractboth took it as a hard
dependency upstream, so a re-vendor without it fails to resolve rather than
quietly building a stale tree. -
Anchored popovers have one owner. Ten surfaces hand-rolled the same panel — a
clamp call, a dismiss call, and anonMountblock pairingaddEventListener
withremoveEventListenerfor some subset of pointerdown / click /
contextmenu / scroll / resize / keydown — and they had drifted into ten
different answers to the same questions, none of them visible at the call
site. The action attaches to the popover node itself, so listener lifetime
becomes popover lifetime: the old blocks lived for the whole component behind
if (!open) returnguards, so a component could hold four window listeners
while showing nothing, and a node inside{#if open}cannot. What stays at the
call site is what genuinely differs there — the dismiss selector, because what
counts as "inside" is a per-surface fact and most sites must count their own
trigger or the opening pointerdown closes the panel; focus restoration; and
keyboard ownership, since a menu with roving focus already owns keydown and
Escape is one branch of it. -
The task sheet's repository control names its links instead of counting them.
The closed trigger draws a chip per linked repository, primary first and
starred, with the summary sentence beneath for what chips cannot say — a
remainder, or a link with no primary chosen. Its two rules moved out of the
markup intotaskRepositories, where they can be tested: a filter never hides
a linked repository, because hiding a chosen row makes the control lie about
what the task links to; and rows keep catalog order, because sorting linked
rows to the top moves a row out from under the pointer that just checked it. -
A task handed to an agent now says what to keep and what to ask with. The
preamble prepended to every copy — draft or saved brief — is the whole
prompt: a run started from the handoff form sends only the task identity to
Manvi, which reads the rest from the store, and a clipboard copy lands in a
session that has never seen this repository. So everything the two agent
guides say about DevMap and GitPulse was unavailable on exactly the path
where an agent is least oriented. It now names both — thegitpulse-insights
andgitpulse-collisionsskills with thegitpulse_*MCP tools, the five
devmapskills with thedevmap_*MCP tools, the absoluterepo_pathevery
call needs, and this repository's own rule that a query which was unavailable,
truncated or empty is not evidence of absence. The skill names are asserted
against the directories that ship them, so the list cannot fall behind the
package. It also says the thing the board's own tests have always been
written around: the author's wording is evidence, not a first draft to tidy. -
Preserving the author's meaning moved from one on-device drafting kind to the
block all three are built from. It sat only onimprove, which left out the
two kinds that need it most —draftwrites the first version of a task from
raw notes, andextractis under explicit instruction to leave material out,
which is precisely where a model drops the error code the note was written to
record. The contract asserts the structure rather than three strings: the
kinds are derived from the Rust and TypeScript validators and cross-checked,
and every branch may only add to the shared rules, so a fourth kind cannot
reintroduce the gap. -
The two
@Guideannotations no longer contradict the instructions above
them. A guide steers the structured decoder token by token while the
instructions are only something the model read, so where they disagree the
guide wins — and both of them disagreed. "At most 12 words" is a budget a
title cannot always pay: a note whose whole point is an error code and a
symbol name spends most of it on the evidence, so the model bought the word
count by rewording the identifier. And "two to five sentences" is a floor,
which on a thin note is an instruction to invent — it repealed "if the notes
are thin, stay general rather than guessing" from the layer that is actually
enforced. The title guide now states the bound that is really checked (300
characters, ininterpretand again in the store) and says to go longer
rather than reword evidence; the description guide caps at five sentences
with no floor. -
The on-device prompt names the field that is not being written. Asked for a
description with a title already present, the model would restate the title
as the opening sentence; asked for a title, it would spend effort proposing a
description the bridge then discarded. The clause appears only when that
other field is actually in the prompt, because telling a model to leave a
title alone when none was supplied asserts that one exists. -
A repository or label list that had to be shortened says how many it left
out. Eight of forty presented as a closed list is how a model comes to write
"affects both repositories" about a task that spans five more. -
The offer to widen a pre-repository approval moved above the branch list. It
lived inside the worktrees panel, beside the rows it is about; adjacency read
well and placed it badly. The sidebar body is one scroller and the branch list
above it has no height cap, so on a repository with many branches the banner
sat below the fold — and the refusal that sends people looking for it named a
control they could not see. The refusal text now names the top of the sidebar,
where the control actually is. Because the offer and the rows it affects are
no longer one component, a grant made up there has to reach them:
repoStore.refreshcannot carry it, since it re-hydrates under the existing
generationandgenerationis precisely what panels compare to tell a stale
response from a current one. The signal is its own store, and a counter rather
than a flag — two extensions in one session are two events, and a boolean that
has to be reset has a window in which it is wrong. -
A worktree row gives its name a line of its own. The single-line row could not
hold the name, the session chips, the branch and the action rail at once: the
name was the only flex item among shrink-0 chips, a branch capped at 90px and
a permanently reserved rail, so it was the only thing that could give — and it
gave all of it. Measured at the 360px default,
gitpulse-prompting-extraction-c8e628rendered in 79px of the 210px it needs;
at the 264px minimum it rendered in 0px and the branch painted straight
through the change count. Line two carries the chips, the branch and the
count, and the armed remove confirm moves there too — under the button that
armed it, instead of splicing a 119px sentence into the name's line at the
exact moment the reader needs to know which worktree is about to lose work.
The right-edge mask is load-bearing rather than tidiness: the chips are
shrink-0 and would otherwise paint through the row's edge, and the mask bites
only when content actually reaches the last 14px, so a clipped row looks
clipped instead of looking like a shorter branch name.
Fixed
-
The DORA change-failure rate and restore time no longer present a capped
commit scan as the whole window. Both are derived from onegit logthat was
fixed at-n 200, so on this repository the rate covered the newest 200 of
532 commits in the default 90 days — 38% of the window — beside a card
reporting release tags across all of it, andgit log -n Nreturns the
newest N, so the cut skewed both numbers recent.cfr_sample_commitscould
not surface this: it separates "measured" from "nothing to measure", but
200is a plain positive number whether it is the whole window or a slice of
it. The cap bought nothing to justify the blind spot — measured over a
25,000-commit history, the scan costs 0.01s capped and 0.07s uncapped,
against the 0.32s the lead-time loop already spends ongit describe— so it
now matchesMAX_PULSE_COMMITSat 25,000, and the report carries
commit_scan_truncatedwith the window total behind it when the cap is still
reached. Both cards say when they read only the newest part of the window,
because one scan feeds them both. The uncapped count is only taken when the
scan actually hits the cap, and a count that cannot be read leaves the
truncation flag set rather than letting a partial scan report itself complete. -
The DORA change-failure card no longer reports an empty window as
0%. An
empty commit window, a repository with no commits, and a shallow clone all
left the rate at0.0with nothing to divide, and the card rendered that
beside three measured numbers — a check that could not run reading exactly
like one that ran and found nothing wrong. The sibling restore-time card had
always declined to invent a number here; the failure rate could not follow it,
because neither of its two existing signals can say "nothing to measure":
is_cfr_approximationis set unconditionally at both construction sites, so
it only ever means "heuristic", and unlike a restore time,0%is a
legitimate answer when commits were examined and none were reverts. So the
report now carries the denominator,cfr_sample_commits, and zero is the
sentinel: the card shows—and says there were no commits in the window to
examine. A measured zero still shows0%, and now names the sample it was
measured over rather than leaving the reader to assume the whole window was
read. -
The change-failure card's
Heuristicpill was the literal word rather than
the flag.is_cfr_approximationcrossed the wire, was set by Rust, and was
read by nobody, so a measured rate would have arrived still labelled a guess —
and a stale label on a number is worse than no label, because it is believed.
The pill and its caption now both follow the flag, as the restore-time card
beside them always has. -
A repository approved before 1.1.0 made the repository the unit of trust kept
every one of its worktrees refused, with nothing anywhere offering to fix it.
The approval still admitted the checkout it named, so the app reported the
repository as trusted and never asked again; the only way to extend it was to
open a linked worktree as a repository, which nothing suggested. Worktree
comparisons, collision checks, and the fleet view quietly dropped every
sibling — honestly, saying each time that they had not been read, but with no
way out. Trust is now reported as a scope rather than a yes/no, the worktrees
panel offers to extend an approval that predates worktree coverage, and the
refusal a linked worktree returns says so instead of telling you to approve a
repository you already approved. Approvals are still never widened by being
read: extending is a decision you take, through the same dialog. -
Installation health no longer invents a fault. GitPulse resolves
devmap
throughPATHplus the GUI-launch fallback dirs, but spawned it with the
PATHit inherited — under a Dock or Finder launch, launchd's
/usr/bin:/bin:/usr/sbin:/sbin.devmap doctorresolves the baredevmap
command that host MCP configs name against its ownPATH, could not find the
binary GitPulse had just resolved out of~/.local/bin, and reported "host
MCP config names a devmap path that is not a file: devmap — this is not
version skew". Nothing was wrong with the install; the check could not run
and named a cause it did not have. Every child that reaches the bounded
runner now gets the extendedPATH, so this is fixed fordevmap, the
tool-install probes and thecurl/tar/cargo/goladder at once — a
caller that chose its ownPATHstill keeps it. -
A warning
devmap doctorraised could reach a panel that then said it "found
nothing to report". The warning fields were written out by hand and devmap
had six, sostray_state_warningwas dropped. They are swept out of the
payload now: unrecognised fields are shown last rather than discarded. -
Warnings are bounded before they are rendered, and say so with both numbers
when they are shortened or the list is capped.stale_server_warning
enumerates one process id per runningdevmap mcp— 84 of them on the
machine that reported this — into a single line. -
npm run mcp:doctorno longer reports a clean pass over binaries running
older code.gitpulse-mcpandgitpulse-hookreport only their package
version, which is a release identity: it does not move when a fix lands
between releases, so the doctor compared 1.1.0 against 1.1.0 and saidok
for a hook built the day before the repository-trust fix — while that hook
went on handing users the pre-fix refusal.mcp:installnow records a digest
of the sources it compiled, and the doctor re-derives it as a third verdict.
An install nobody recorded reports unverifiable rather thanok, because
one that was never checked must not read like one that was checked and
matched. -
A refusal embedded in the collision-guard notice no longer brings its own
full stop with it, which was rendering "…covers all of them.. Other
worktrees may hold…" in every agent session inside an untrusted worktree. -
The walkthrough no longer waits on a frame that may never arrive. Its
spotlight was positioned from a measurement taken inside
requestAnimationFrame, so on a host that had stopped painting — an occluded
or minimised window, a background tab, a headless runner — the measurement
never happened: no highlight was drawn, and the step said "The Open menu is
not currently visible" about a control that was on screen. The measurement is
now bounded, so it lands whether or not a frame does. -
Hook payloads from Cursor are read and answered in Cursor's schema rather
than Claude's, which had broken the session brief in both directions. Cursor
nativesessionStartsendsworkspace_rootsand usually nocwd, so every
Cursor session looked like a session with no repository and got no brief at
all; and Cursor injects only a top-leveladditional_context, so a brief
that was generated parsed as valid JSON and was then ignored. Cursor
preToolUseis a permission hook whose schema is not Claude's either, and
emitting Claude's nestedpermissionDecisionthere blocked the tool — it now
emits nothing and fails open. The host is chosen by a non-null
cursor_version, not by event-name casing: Claude plugins running on Cursor
still send camelCasesessionStart. Claude's own output is unchanged. -
An agent worktree's kind and slug come from one scan instead of two searches
that could disagree. The kind was found by locating/.<name>/worktrees, the
slug by independently finding the first/worktrees/in the whole path — so
any ancestor directory namedworktreescaptured it, and
~/worktrees/app/.claude/worktrees/session-abcreported its slug asapp.
Every session under such a parent collapsed into one label, which is the
exact merging the slug exists to prevent. Two further shapes were misread on
the way:.GIT/worktreesis git's own metadata store on the
case-insensitive volumes macOS and Windows ship by default, and was reported
as a real agent of kindGIT; and/repo/../worktrees/xreported an agent
of kind.. Windows separators are matched in the same pass, so an agent
worktree there is no longer labelled hand-made. Both implementations of the
rule — the Work view's and the backend's — now answer to one shared corpus,
so they cannot drift apart silently again. -
An agent-session count taken from a capped sample no longer reads as an exact
total. The worktree walk is bounded, and a kind whose only worktrees fell
past the cap was missing from the list outright rather than undercounted. The
cell now carries the bound with it. -
The rule deciding whether a vendored manifest still inherits from a workspace
has one owner. The vendor contract test carried its own copy, and when the
copy invendor-crates.mjslearned that a description is prose, the test's
did not — sodc-proc, which describes itself as "shared by every place this
workspace shells out", vendored cleanly and then failed the very contract
that confirms it vendored cleanly. -
The worktree trust refusal named a control the UI does not have. It said
"Extend Trust" while the button read "Extend trust to every worktree" in the
left sidebar's Worktrees section, so a reader handed those words found nothing
on screen and concluded the control had been removed — one of them read the
source to find a banner that was visible the whole time. Naming a control by
the wrong name is worse than naming none. The message is also shorter: the
sentence before it already says approving any working tree covers the family,
so restating the cheap path said the same thing twice. A new contract test
pins the string against the button's own label, because Rust owns the message
and Svelte owns the button and nothing else compares them. -
Choosing which suggested fields to accept no longer marks the task edited. The
sheet marks dirty from any change inside its form, so the unsaved-edits guard
was refusing the very acceptance the checkbox was selecting. -
The platform-vocabulary guard reads a comment for its whole length, and
resumes scanning the moment it ends. It decided comment-or-not one line at a
time, and the house style indents a block comment's continuation lines as prose
rather than gutter-marking them, so line two was read as template text and a
platform named in the explanation was reported as output. 629 comment lines in
the scanned trees read that way; the guard was green only because none of them
happened to name a platform, and the first that did was reworded to appease the
bug. The reverse half matters as much —<!-- why --> ⌘Kreally does print a
glyph, and the old line test skipped that line whole. The one way comment
tracking can fail silently is an opener it should not have believed, which
skips the rest of the file as prose, so the scanner now reports whether it
closed what it opened and every scanned file is asserted to do so: a blinded
scanner must not read as a clean one. -
A draft copy that had to cut an oversized description said so instead of
ending mid-sentence. Notes are deliberately kept whole until the save
boundary reports them, so an unsaved draft can carry more than a saved task's
64 KiB — and the copy silently truncated to that cap, leaving an agent to
answer as though it had read the rest. Same contract the model budgeters in
ai/prompt.rsalready follow: cut, and say so inside the text that was cut. -
The Swift bridge bounds a generated title in the unit everything downstream
counts in.String.prefixcounts grapheme clusters;interpretand the
store both count Unicode scalars, and those are not the same number — one
family emoji is one grapheme and five scalars. So the cut was wrong in both
directions: a 300-grapheme title could carry 1500 scalars and be refused by
the store after the model had already run, while on plain ASCII it landed
exactly on 300 and made that refusal unreachable, so an over-long title was
silently clipped instead of caught. The cut now counts scalars and stops on a
whole character, so it never severs a combining mark or a ZWJ sequence. -
The assist button says which operation will actually run. The drafting kind
and the button's verb were two separate readings of the same three fields, so
they disagreed: with notes present the button read "Draft" while the request
carriedextract, and on an empty task it read "Improve" while the request
carrieddraft. Neither expression was reachable from a test — both lived
inside the component — so nothing caught it. The verb is now derived from the
kind rather than re-read from the state, and both live intaskEnhancewhere
they are covered. -
The archive-separation plan names the commit its vendored store was re-synced
to. The closure moved to0944ef51and the plan still namedb366f42, so the
plan's own drift guard was red. Its other five checks re-read the new source
and still hold —work_itemshas noarchivedcolumn,put_itemaccepts
exactly its eighteen named fields,list_itemsfilters on exactly six, and
the schema version is unchanged — so only the reference had gone stale. -
A task harness check asserted the button verb a bug used to produce, and so
failed on both renderers. On a brand-new task — no notes, no title, no
description —draftingKindreturnsdraftanddraftingVerbrenders that
as "Draft", but the check looked forbutton("Improve with Manvi"), and the
harness matches button text exactly, so the lookup returnedundefined.
"On an empty task it read 'Improve' while the request carrieddraft" is the
precise disagreementdraftingKind/draftingVerbwere extracted to end, so
this line had been pinning the defect rather than the repair — it would have
gone green again only if the bug came back. It now asserts "Draft with
Manvi", which fails if the verb ever drifts off the kind again.Worth recording because of how it was nearly dismissed: it was carried in
notes as a known pre-existing failure, on the strength of it also failing
against a reverted baseline. That only established it predated one branch.
The same check reads"pass":truein the 2026-09-14 CI log, so it was a
regression with a cause, not a standing exception. "Fails on the baseline
too" is not evidence that a failure is expected.
Verification
Built and installed on macOS (Darwin 27.0.0, aarch64) from main at 28e6f3a,
after three branches were merged into it: the App Hosting rollout work, the
empty-window change-failure fix, and the capped-commit-scan disclosure. The
tagged commit differs from that built tree only by this changelog entry.
-
npm run ci:local→ exit 0, 15 passed · 0 failed · 0 skipped of 15 gates.
Every number below comes from that single run.An earlier run of the same gates is not reported here, and the distinction
matters more than the result did. Sources were edited while it was still
running, so its later gates describe a tree that no longer exists; reporting
them would have been the exact substitution this release is about — a check
that could not run reading like one that ran and passed. It was discarded and
the gates were re-run from a clean tree. -
npm run check→svelte-checkover 5087 files: 0 errors, 0 warnings, 0
files with problems, thentsc --noEmitclean. -
npm run coverage→ 7100 passed, 0 failed across 504 test files. -
Rust tests under
cargo llvm-cov→ 2584 passed, 0 failed, 19 ignored
across 79 test binaries, summed from the individualtest result:lines
rather than read off a tail: this shell is zsh, where a pipedtaillaunders
both the counts and the exit status. -
npm run test:browser:allandnpm run test:webkit:all→ 894 checks across
13 harnesses on each engine, 1788 in total, headless Chrome 152 and
WKWebView both green. The Firebase harness accounts for 85 of each engine's
checks, up from 36, covering the rollout confirmation and the capability probe. -
npm run check:coverage→ floors hold: frontend lines 96.17%
(12842/13354), frontend branches 89.47% (12554/14032), Rust lines
85.36% (69057/80902). -
npm run check:ipc→ 224 handlers, 0 orphaned, 0 missing.
npm run check:types→ 67 contracts, 167 structs, 1168 fields, 0 drift.
Both counts moved twice as branches landed and were read back from the
checkers at each step rather than added up by hand; the documented-counts
contract passes 13/13 over the four tracked docs. -
npm run check:release→OK: all version sources agree on 1.2.0across all
ten manifests. -
npm run tauri build→ exit 0, first attempt, release profile in 1m 30s.
GitPulse.appandGitPulse_1.2.0_aarch64.dmg(21,974,040 bytes), every
nested helper (gitpulsed,gitpulse-mcp,gitpulse-hook) ad-hoc signed
before the outer bundle.The install was verified rather than assumed:
diff -rbetween the built
bundle and/Applications/GitPulse.appreports them identical, and all four
binaries match by SHA-256.codesign --verify --deep --stricton the
installed copy: valid on disk, satisfies its Designated Requirement, all
three helpers validated, designated cdhasha755a292ca1f8749…. Not notarized
— no Apple credentials in the environment, as usual for a local build.hdiutil infowas checked for an attacheddmg.*volume before the build,
not after one failed; only system simulator runtimes were mounted. -
npm run mcp:install→gitpulse-mcpandgitpulse-hookreplaced at 1.2.0,
provenance recorded at source digest36f4f49a2e01672c…over 676 files.
npm run mcp:doctor→ OK on all three claims.The digest is the whole reason this was caught. Before the reinstall, the
doctor reported the version it wanted (1.2.0) and the store schema it wanted
(22) on both sides and still failed, because the recorded digest was
06abf2cc16fb5fed…over the same 676 files while the tree had moved to
36f4f49a2e01672c…. Version is a release identity and does not move between
releases, so two of the three verdicts would have passed a binary running
pre-merge code. -
Superseded build evidence was pruned to the single entry whose chunks match
the shipped build, 39 MB down to 9.8 MB. Revision alone could not identify
it: two builds of the same commit28e6f3aproduced different chunk hashes
(main-1nI-zcB3.jsandmain-CJc8Sj5r.js), so an entry can carry the right
revision and still not be the one that shipped. The surviving entry was
matched two independent ways —dist/index.html, and the chunk name embedded
in the installed binary itself, which agree onmain-CJc8Sj5r.js. -
Consolidation removed seven worktrees and three branches. Each branch was
deleted withgit branch -d, which refuses an unmerged branch, so the merge
was confirmed by the tool rather than asserted; every worktree HEAD was
separately checked to be an ancestor ofmainwith zero unique commits, using
a test carrying its own discrimination case so that an always-true check could
not pass for one. About 57 GB was reclaimed: 38 GB of worktrees, 18.7 GB
oftarget/debugand stale coverage instrumentation, and the pruned evidence.
What is not verified here. Notarization is absent, as above. Nothing was
pushed: main is ahead of origin/main and the tag is local, so the
CI-provenance precondition in release:ready — a successful push run of
ci.yml and coverage.yml on the tagged commit — is not satisfied, and no
draft was prepared. The App Hosting panel, including the new rollout action, was
exercised by its own harness and unit tests only; no live Firebase project was
contacted and no rollout was created, deliberately, because creating one changes
what production serves and both listings enable the API on the project they
read. docs/PROMO.md needed a handler-count correction that is not in any
commit: .gitignore excludes it, and its contract test reports skipped rather
than passed where the file is absent.
Changed — vendored crates
-
The vendored DevCouncil closure is re-synced to
devcouncil@0944ef51, moving
the tendc-*anddevmap-*crates from 0.2.2 to 0.2.3.vendor:checkhad
been failing — every cratelocal: clean,upstream: drifted— because the
record was pinned atb366f420while that repository'smainmoved on; it
now reportsupstream: matchesfor all eleven and exits 0.
check:vendor-schemaconfirms the vendored store schema is still 22, so this
is not a migration: an existing store opens unchanged.Only the ten vendored packages moved in
Cargo.lock, confirmed by name — a
blanket 0.2.2 → 0.2.3 rewrite is the shape that has corrupted an unrelated
dependency sharing that version before, and cargo resolved this rather than a
text substitution. -
The end-to-end half of the pre-repository-approval test asserts
EXTEND_TRUST_CONTROLinstead of its own copy of the button label. Spelled as
a literal it passed while the message and the button disagreed, then failed on
the commit that made them agree — the exact drift the constant exists to
prevent. The unit test beside the constant already referenced it; this one was
missed, and the suite rather than the diff is what found it.
Hardened — release automation
- Remote release verification (
release-state.mjs) is hardened against three
failure points before deployment:- Line endings: notes round-trip comparison during
finalizenormalizes CRLF
(\r\n) to LF (\n), preventing spurious refusals when GitHub's Releases
API converts markdown newlines. - Asset digests: SHA-256 digest inspection accepts uppercase hexadecimal and
normalizes to lowercase before snapshot comparison. - HTTP 404 handling: the API helper allows missing endpoints unconditionally
whenallowMissingis true, regardless of child process exit status.
- Line endings: notes round-trip comparison during