Skip to content

release: 1.0.0 - the guide, the wiring test, and the security pass behind the version number - #17

Merged
jaymar921 merged 9 commits into
mainfrom
feat/1.0.0
Aug 20, 2026
Merged

release: 1.0.0 - the guide, the wiring test, and the security pass behind the version number#17
jaymar921 merged 9 commits into
mainfrom
feat/1.0.0

Conversation

@jaymar921

Copy link
Copy Markdown
Owner

Takes HirayaCoder from 0.9.0 to 1.0.0.

The version number is the headline and it is a claim about stability, not features:
this PR adds no tool, no permission, no network call, and no dependency. pathGuard,
permissionGate, scriptRunner and the allow-list are untouched. What it adds is the
things a release needs before strangers install it.

Merging this does not publish anything. The GitHub Release is created by CI on a
v1.0.0 tag push; the Marketplace listing stays a deliberate manual vsce publish.
No Marketplace token is stored in this repo.

What's in it

A setup guide in the panel — a Guide button in the chat header opening the four
setup steps and, more usefully, what to expect: a task takes 1–5 minutes on a laptop, a
refused write is usually the checks working, a small model is good at one file and bad at
a whole app. The panel previously assumed its reader had read the README; for the release
where strangers arrive, that is the wrong assumption. It renders locally — unlike every
other control here it has nothing to ask the host for — which is also why it adds no new
entry point on the privileged side of the boundary.

A test that every button does somethingwebviewWiring.test.js, 52 checks. Reads
index.html, main.js and chatTab.js as text and asserts every interactive element
resolves, every button can be activated (own listener, delegated container, or the form
it submits), and the message protocol closes both ways: 13 webview→host types, 16 back.
A dead button was the one UI defect nothing here could see — component tests assert node
shape, integration tests drive the host, and neither notices a control nobody listened to.

A ReDoS fix found by measuring, not reading (SAST-014)stepBrief.PATH_TOKEN
scans for filenames with /g, and scanning for a token that isn't there costs one attempt
per start position, so an unbroken run of word characters was O(n²):

Input (a single run of a) Before After
3,200 chars 23 ms
51,200 chars 6.1 s
204,800 chars 85.2 s 244 ms

That's the extension host frozen for 85 seconds, reached by pasting a data URI, minified
line or hash into the composer. core/commonSense holds the same expression and has
bounded it since it was measured there; stepBrief inherited the comment explaining that
a single match is linear — which is true, and answers a different question. Bounding each
segment to 120 chars restores linear scanning with identical output on every path in this
repo. Pinned by a timing test.

A 1.0.0 that can't land as a pre-release — the classification was already derived from
the version (0.x and -rc/-beta are flagged, nothing else), so the normal path was
fine. The hole was the other path: where a release for the tag already exists, the flag was
only ever added, never cleared, so a hand-drafted pre-release would stay one forever.
Both directions are now explicit, the edit is unconditional, and full releases get
--latest — which is what /releases/latest resolves to and therefore what install
instructions point at. Pinned by a test, since this is checkable exactly once per tag.

Marketing — six image sources bumped to v1.0.0 and re-rendered, plus four new 1:1
launch ads with captions in docs/images/ADS-1.0.0.md. Also removes
docs/images$name.png, 71 KB of an unexpanded shell variable committed by accident in
0.8.0 and referenced nowhere.

Docsdoc/PUBLISHING.md split into a one-time first publish (Part A) and the
repeating release (Part B), with a full account of what a version bump touches beyond
package.json. Its manifest sample had drifted from the real one in five fields including
the licence, and its CI figures said 573 unit / 12 integration against an actual 1,545 / 16.

Deliberate non-changes, and why

  • The install route still points at GitHub Releases, not the Marketplace. The tag is
    cut before the listing exists. capabilities.html shipped a "search the Extensions
    view" footer through 0.7.0 when there was nothing to find; ad-4-launch.html was drafted
    with the same mistake and corrected before rendering. PUBLISHING.md Step 9b lists the
    four files to flip on the day the listing is live.
  • The "After" badge in knows-what-it-has.html stays at v0.9.0. It labels the release
    a measurement was taken on. Bumping it would make the image claim a number it never
    measured.
  • The "New in 0.9.0" tag was removed, not moved. It travels to whichever card the
    release rewrote, and 1.0.0 rewrote none of them.
  • ESLint still reports 22 detect-unsafe-regex warnings after the fix, including the
    bounded expression — safe-regex objects to the shape, not the behaviour. The count
    didn't move and an 85-second freeze was removed, which is the argument for reviewing
    these rather than counting them.

Security

Full report: security/sast-report-2026-08-20-1.0.0.md. Zero production dependencies,
zero advisories (npm audit --omit=dev and full), no Critical or High findings. All 22
detect-unsafe-regex expressions in app/ were timed against purpose-built worst-case
inputs at four doubling lengths: 20 linear, 1 already bounded, 1 was SAST-014 above. The
4 retire.js findings are DOMPurify inside .vscode-test/ — Microsoft's bundled copies in
a gitignored directory, never packaged.

Verification

  • npm run test:all green: lint (0 errors), 1,545 unit, 16 integration against a
    real VS Code.
  • Modes re-verified at both layers: toolRegistry.forMode withholds mutating tools
    outside Agent, AgentSession._execute refuses any name it wasn't offered, and Ask
    builds no loop at all.
  • npm run package → 515.39 KB, 98 files, with guideCard.js included and
    docs/images/** excluded, so the new ads add nothing to the .vsix.
  • Windows-specific: releaseWorkflow.test.js initially failed on Windows CI only — a
    fixed-string match containing \n against a CRLF checkout. Fixed by normalising
    newlines on read, and verified by converting release.yml to CRLF in place and
    re-running rather than assuming.

Reviewing

The riskiest change is the one-character-class edit in app/agent/stepBrief.js:80 — it
alters what a shipped regex matches. Verified identical output against the benchmark
briefs and every path in this repo's docs, and covered by two tests, but it's the line to
look at first.

…ind out how

The panel assumed its reader had already read the README. For a 1.0.0 release that
is the wrong assumption to ship: the most likely reader is someone whose first run
went nowhere because Ollama is not running, or who is four minutes into a task and
does not know whether that is normal.

`components/guideCard.js` holds both halves — the four setup steps and, more
importantly, what to expect once they are done. The expectations are blunt on
purpose. A user told to expect ChatGPT concludes the extension is broken; one told
a task takes 1-5 minutes on a laptop waits for it.

It renders locally rather than posting to the host, because unlike every other
control here it has nothing to ask for: the text is identical on every machine. And
it is a card in the transcript rather than a modal, so it can be read beside the run
that prompted it instead of covering it.
… see

Asked to confirm every control in the panel is clickable and does something, the
honest answer was that no test could tell. The component tests build nodes and
assert their shape. The integration tests drive the host. Neither notices a button
in index.html that nobody listened to, or a control posting `attach-flie` into a
switch that falls through to `default` — both of which look entirely normal on
screen until someone clicks.

So this checks the seams as text: every interactive element is resolved in main.js,
every button has a way to be activated (own listener, delegated container, or the
form it submits), and the message protocol closes in both directions. Also the
reverse — a handler delegating on [data-mode] when no button carries it is the same
dead control seen from the other side.

52 checks, covering 13 webview-to-host message types and all 16 coming back. The
extraction regexes are guarded by a count assertion, since a broken regex would
otherwise make every check pass against an empty set.
…o the composer

SAST-014, found by measuring the 22 regexes ESLint flags as unsafe rather than
reading them. `stepBrief.PATH_TOKEN` scans an item's text with /g for filenames, and
scanning for a token that is not there costs one attempt per start position — so an
unbroken run of word characters is O(n^2). Measured on a single run of `a`, which is
what a pasted data URI, minified line or hash looks like to this expression:

    3,200 chars      23 ms
   51,200 chars     6.1 s
  204,800 chars    85.2 s     extension host frozen throughout

`core/commonSense` has the same expression, and has documented and bounded this since
it was measured there. `stepBrief` copied the comment about a single match being
linear — which is true — and not the bound, so nothing here contradicted the code; it
just answered a different question than the one that mattered.

Bounding each segment to 120 characters caps the work per start position and makes
the sweep linear: 244 ms at 204,800 characters, down from 85 s. Verified identical
output on the benchmark briefs and on every path in this repo's own docs, and the
timing is pinned by a test, because the bound is the only thing between that input
and the freeze.
…ing yet

Four 1:1 cards at 2160x2160, rendered from HTML like the README images and sharing
their palette so the set reads as one product. One argument each, in the order a
carousel should carry them: your code never leaves your machine (the hook), agentic
down to 1B (the proof it works), you approve every write (the objection), 1.0 is out
and here is how to start (the ask).

Captions live beside them in ADS-1.0.0.md — one per image, plus a single caption for
posting all four together with five hashtags.

Two things worth recording. Ad 3's diff header said +6/-2 over a diff showing five
added lines and one removed; a mock that gets its own arithmetic wrong is a bad advert
for a tool whose pitch is that it shows you the diff. And ad 4 was drafted with
"Search HirayaCoder in the VS Code Extensions view" in the footer, which is exactly
the instruction capabilities.html shipped through 0.7.0 and which could not work,
because the tag is cut before the Marketplace listing exists. Both fixed before
anything was rendered; the second is now a note in ADS-1.0.0.md and src/README.md
saying what to swap on the day the listing is live.
…pre-release

Version badges to v1.0.0 and re-rendered. Three deliberate non-changes:

The "After" badge in knows-what-it-has.html stays at v0.9.0. It labels the release the
measurement was taken on, not the current version, and bumping it would make the
picture claim a number it never measured.

The "New in 0.9.0" tag is removed rather than moved. It travels to whichever card the
release rewrote, and 1.0.0 rewrote none of these six — it is the release that
stabilises them. Leaving it would have let this release take credit for the last one's
work; moving it would have needed somewhere true to move it to.

The install route stays GitHub Releases. The version number and the Marketplace
listing are separate events and the tag is cut before the listing exists, so the route
that is true today and stays true afterwards is the one to ship. What did change is
the wording: "Pre-release" was about distribution, not stability, and a 1.0.0 calling
itself a pre-release reads as a mistake. The footers now say "Free & MIT" and the
hero's note says "Free and MIT-licensed" — neither asserts a listing that is not there.
src/README.md records what to swap on the day it is.

(docs/images$name.png — 71 KB of an unexpanded shell variable committed by accident in
0.8.0 — was removed in the previous commit, where the staged deletion rode along.)
The classification itself was already right and already automatic: the workflow reads
the version from package.json and flags `0.x` or any `-suffix` build as a pre-release,
so 1.0.0 comes out as a full release with no switch to remember. Verified against the
versions either side of the line.

The hole was one path further down. `FLAGS` was empty for a full release, which is
correct for `gh release create` — it defaults that way — but the same variable is
reused where the tag already has a release, and there it was guarded by
`if [ -n "$FLAGS" ]`. So a release drafted by hand as a pre-release, or left behind by
a half-finished earlier run, would keep that flag forever and the workflow would
silently agree. 1.0.0 is exactly the release where that is wrong, and nobody would
think to check it.

Both directions are now stated: `--latest` on create, `--prerelease=false --latest` on
edit, and the edit is unconditional. `--latest` also matters on its own, since
/releases/latest is where install instructions point.

Pinned by a test, because this is checkable exactly once — on the tag push, in a job
that cannot be re-run against the same tag.
…fter"

The guide had one linear path with a note that steps 1-3 were one-time. For the
release that actually does those steps once, that is the wrong shape: a first publish
and an update share the commands but not the risk, and the parts that are hard to undo
were not marked as such.

Now Part A is the one-time publisher and token setup, Part B is the repeating release,
and a table up front says which one you are doing. A second table separates the three
things people conflate at a release — the GitHub Release (automated, reversible), the
Marketplace listing (manual, permanent), and the version number — because confusing
them is how this goes wrong.

The substantial addition is Step 5. Bumping the version means ten image sources, the
README's hero alt text, the changelog and a new SAST report, none of which fail loudly
when they go stale — they just ship a picture reading v0.9.0 on a 1.2.0 release. It
also records the two traps found while doing exactly that this release: one version
badge labels the release a measurement was taken on and must not be bumped, and the
"New in" tag either moves to the card the release changed or comes off.

Step 9b is new and deliberately late: flip the install route to the Marketplace only
once the listing is live, with the four files that carry it listed. Step 10 now states
that release-vs-pre-release comes from the version and is covered by a test.

Also corrected: the manifest sample had drifted from the real package.json in five
fields, including the licence, and the CI figures said 573 unit and 12 integration
tests against an actual 1,545 and 16.
package.json to 1.0.0, which is what the release workflow reads to decide the tag is a
full release rather than a pre-release.

The changelog entry leads with what 1.0.0 does not add — no tool, no permission, no
network call, no dependency — because that is the honest shape of it. The version
number is a claim about stability, and the three things that back it are the guide, the
wiring test, and a SAST pass that measured what earlier passes had read.

The report itself is mostly one table: all 22 expressions ESLint flags as unsafe in
app/, each timed against its own worst-case input at four doubling lengths. Twenty are
linear. One was already found and bounded when it was written. One was SAST-014, and
that only turned up because the sibling module it was copied from had a bound this one
did not. Reading the shapes would not have found it — the comment above it was correct.

Also dates the 0.9.0 heading, which said "unreleased" over a version tagged the same
day.
…ntent

Windows CI: 1,544 passing, one failing, on the one platform the test was not written
on. The assertion matched a fixed string containing `\n`, and this repository pins no
line endings in .gitattributes — so a Windows checkout hands back release.yml with
CRLF and the pattern misses against byte-identical content. It passed locally because
the working copy had just been written with LF.

Newlines are now normalised on read, which covers every assertion in the file rather
than the one that happened to break. The guard assertion also becomes a pattern
instead of a fixed string, so re-indenting that block is a formatting change rather
than a failed release check.

Verified by converting release.yml to CRLF in place and running the suite against it —
5 passing where it previously failed, and 1,545 overall — then restoring LF and
running it again.
@jaymar921 jaymar921 self-assigned this Aug 20, 2026
@jaymar921 jaymar921 added documentation Improvements or additions to documentation enhancement New feature or request fix Bug fixes labels Aug 20, 2026
@jaymar921
jaymar921 merged commit 5b4270d into main Aug 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request fix Bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant