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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ This project follows SemVer.

## [Unreleased]

### Fixed

- **`version:check` catches a stale docs site.** `docs/index.md` opens with a `**vX.Y.Z** is published` banner, which replaced the `**Status:** v` line the release sync and drift check looked for, so neither saw it: 3.3.0 was released on 2026-09-27 with `version:check` green while bashbop.github.io/otito still announced v3.2.0 and had no 3.3.0 entry under What's New. `syncPinnedDocVersion` now rewrites the banner, `findPinnedDocVersionDrift` reports it, and a new `findWhatsNewDrift` fails `version:check` when What's New has no `vX.Y.Z published` entry for the `package.json` version (it is written by hand, so the sync cannot add it). The site gets its v3.3.0 banner and entry.

## [3.3.0] - 2026-09-27

The terminal output gets colour and shape it didn't have before. `otito` resolves one glyph set per run (plain Unicode by default, ASCII in CI, emoji opt-in) and builds tables, trees and coloured lists from shared string-building primitives instead of ad-hoc strings; every bullet and list item now dims its marker the way a box border is dimmed, matching the headers, boxes and closing line that already had colour. `otito install` prompts a human at a TTY for how to install, with a short personalized pitch first, while every non-interactive caller (`--yes`, `--json`, CI, agents) is unaffected. No command, field or schema was removed.
Expand Down
12 changes: 11 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

> For teams that want any coding agent to produce evidence a human can trust before merge.

**v3.2.0** is published to npm, GitHub Releases, and the official MCP Registry. Òtítọ́ is a Bashbop Ltd product, MIT licensed.
**v3.3.0** is published to npm, GitHub Releases, and the official MCP Registry. Òtítọ́ is a Bashbop Ltd product, MIT licensed.

---

Expand All @@ -19,6 +19,16 @@

## What's New

!!! tip "v3.3.0 published (2026-09-27)"
The terminal output gets colour and shape it didn't have before. No command, field or schema was removed.

- The default terminal look is plain Unicode (`✓ ! ✗`, box drawing, arrows) with no emoji; CI, `NO_EMOJI`, `--no-emoji` and `TERM=dumb` keep ASCII, and `--emoji` opts back in. Tables, trees and lists come from shared primitives, and every command that prints for a person ends with one closing line such as `Verified.` or `Runs without errors.`
- `otito install` asks a person at a terminal how to install, while `--yes`, `--json`, CI and agents get the same output as before.
- `context_pack` and `change_impact` stop ranking translation catalogs, file extensions and test notes ahead of the code a request names; a path or symbol named in the request is pinned as a required owner.
- A version bump that reaches `main` is tagged and released by the new `Tag release` workflow, without a hand-pushed tag.

[npm v3.3.0](https://www.npmjs.com/package/@bashbop/otito/v/3.3.0) · [GitHub Release](https://github.com/BASHBOP/otito/releases/tag/v3.3.0) · [MCP Registry](https://registry.modelcontextprotocol.io/?q=io.github.BASHBOP%2Fotito)

!!! tip "v3.2.0 published (2026-09-26)"
The gates gate what they are given. No command, field or schema was removed.

Expand Down
6 changes: 5 additions & 1 deletion scripts/check-version.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { findPinnedDocVersionDrift } from "../src/lib/version-docs.js";
import { findPinnedDocVersionDrift, findWhatsNewDrift } from "../src/lib/version-docs.js";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const packageJson = readJson(path.join(root, "package.json"));
Expand Down Expand Up @@ -52,6 +52,10 @@ function checkPinnedDocVersions(filePath, expectedVersion) {
for (const issue of findPinnedDocVersionDrift(content, expectedVersion)) {
fail(`${relativePath} ${issue} (run \`node scripts/sync-server-version.mjs\` to resync)`);
}

for (const issue of findWhatsNewDrift(content, expectedVersion)) {
fail(`${relativePath} ${issue} (add it by hand from CHANGELOG.md)`);
}
}

function readJson(filePath, { optional = false } = {}) {
Expand Down
39 changes: 33 additions & 6 deletions src/lib/version-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@

const VERSIONED_COMMAND = /(npm install -g @bashbop\/otito@|npx -y @bashbop\/otito@)(\d+\.\d+\.\d+)/g;
const STATUS_LINE = /(\*\*Status:\*\* v)(\d+\.\d+\.\d+)/;
// The landing page's banner, "**v3.3.0** is published to npm, ...". It
// replaced the "**Status:** v" line, and neither the sync nor the drift
// check knew its shape, so the 3.3.0 release shipped with the site still
// announcing v3.2.0.
const PUBLISHED_BANNER = /(\*\*v)(\d+\.\d+\.\d+)(\*\* is published)/;
const WHATS_NEW_HEADING = /^## What's New\s*$/m;

// Rewrites pinned `@bashbop/otito@X.Y.Z` install/verify commands and the
// docs "Status" line to the current release. Deliberately does not touch
// docs/index.md's "What's New" section (a per-release changelog entry with
// its own historical npm/GitHub-release links) since neither pattern
// appears there.
// Rewrites pinned `@bashbop/otito@X.Y.Z` install/verify commands, the
// docs "Status" line and the landing page's published banner to the current
// release. Deliberately does not touch docs/index.md's "What's New" section
// (a per-release changelog entry with its own historical npm/GitHub-release
// links) since none of these patterns appears there.
/**
* @param {string} content
* @param {string} version
* @returns {{ content: string, changed: boolean }}
*/
export function syncPinnedDocVersion(content, version) {
const updated = content.replace(VERSIONED_COMMAND, `$1${version}`).replace(STATUS_LINE, `$1${version}`);
const updated = content.replace(VERSIONED_COMMAND, `$1${version}`).replace(STATUS_LINE, `$1${version}`).replace(PUBLISHED_BANNER, `$1${version}$3`);
return { content: updated, changed: updated !== content };
}

Expand All @@ -41,5 +47,26 @@ export function findPinnedDocVersionDrift(content, expectedVersion) {
issues.push(`Status line says v${statusMatch[2]} but package.json version is ${expectedVersion}`);
}

const bannerMatch = content.match(PUBLISHED_BANNER);
if (bannerMatch && bannerMatch[2] !== expectedVersion) {
issues.push(`published banner says v${bannerMatch[2]} but package.json version is ${expectedVersion}`);
}

return issues;
}

// The "What's New" section is written by hand per release, so the sync
// cannot fill it in; this only reports that the current version has no
// entry. Docs without the section (RELEASE.md) report nothing.
/**
* @param {string} content
* @param {string} expectedVersion
* @returns {string[]}
*/
export function findWhatsNewDrift(content, expectedVersion) {
if (!WHATS_NEW_HEADING.test(content)) return [];

const escaped = expectedVersion.replace(/\./g, "\\.");
const entry = new RegExp(`^!!! \\w+ "v${escaped} published`, "m");
return entry.test(content) ? [] : [`What's New has no "v${expectedVersion} published" entry for the current package.json version`];
}
43 changes: 42 additions & 1 deletion tests/version-docs.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { syncPinnedDocVersion, findPinnedDocVersionDrift } from "../src/lib/version-docs.js";
import { syncPinnedDocVersion, findPinnedDocVersionDrift, findWhatsNewDrift } from "../src/lib/version-docs.js";

const WHATS_NEW = `
## What's New
Expand Down Expand Up @@ -73,3 +73,44 @@ test("findPinnedDocVersionDrift ignores docs with no pinned version markers", ()
const issues = findPinnedDocVersionDrift("# Just prose, no pins here.", "1.9.2");
assert.deepEqual(issues, []);
});

// docs/index.md replaced the "**Status:** v" line with this banner, and the
// 3.3.0 release shipped with the site still announcing v3.2.0.
function bannerIndexFixture(bannerVersion, whatsNewVersions) {
const entries = whatsNewVersions.map((v) => `!!! tip "v${v} published (2026-09-27)"\n - notes\n`).join("\n");
return `**v${bannerVersion}** is published to npm, GitHub Releases, and the official MCP Registry.

## What's New

${entries}`;
}

test("syncPinnedDocVersion rewrites the published banner", () => {
const { content, changed } = syncPinnedDocVersion(bannerIndexFixture("3.2.0", ["3.2.0"]), "3.3.0");
assert.equal(changed, true);
assert.match(content, /^\*\*v3\.3\.0\*\* is published/);
assert.match(content, /v3\.2\.0 published \(2026-09-27\)/, "What's New history is untouched");
});

test("findPinnedDocVersionDrift reports a stale published banner", () => {
assert.deepEqual(findPinnedDocVersionDrift(bannerIndexFixture("3.2.0", ["3.3.0"]), "3.3.0"), [
"published banner says v3.2.0 but package.json version is 3.3.0",
]);
assert.deepEqual(findPinnedDocVersionDrift(bannerIndexFixture("3.3.0", ["3.3.0"]), "3.3.0"), []);
});

test("findWhatsNewDrift reports a release with no What's New entry", () => {
assert.deepEqual(findWhatsNewDrift(bannerIndexFixture("3.3.0", ["3.2.0", "3.1.0"]), "3.3.0"), [
'What\'s New has no "v3.3.0 published" entry for the current package.json version',
]);
assert.deepEqual(findWhatsNewDrift(bannerIndexFixture("3.3.0", ["3.3.0", "3.2.0"]), "3.3.0"), []);
});

test("findWhatsNewDrift does not accept a different version that shares a prefix", () => {
assert.equal(findWhatsNewDrift(bannerIndexFixture("3.3.0", ["3.3.01"]), "3.3.0").length, 1);
assert.equal(findWhatsNewDrift(bannerIndexFixture("3.3.0", ["3.3.0-rc.1"]), "3.3.0").length, 1);
});

test("findWhatsNewDrift ignores docs without a What's New section", () => {
assert.deepEqual(findWhatsNewDrift("npm install -g @bashbop/otito@3.3.0\n", "3.3.0"), []);
});
Loading