Run AI coding agents from YAML, send them work, and recover from stalls and crashes. No Hive required. Early software.
Get started →
diff --git a/llms.txt b/llms.txt
index f6b68ce..4772bc7 100644
--- a/llms.txt
+++ b/llms.txt
@@ -9,7 +9,7 @@ Hive Commons also maintains Spektacular (Spek), a spec-driven workflow tool; the
## Projects
- [Hive](https://github.com/hivecommons/hive): control layer for governed, audited fleets of AI coding agents. Docs: https://docs.hivecommons.dev
-- [Crux](https://github.com/hivecommons/crux): starts AI coding agent CLIs in tmux from a YAML definition, sends them work, and restarts or pauses agents that stall. First release v0.1.0 (early software). Docs: https://docs.hivecommons.dev/docs/crux/overview/introduction
+- [Crux](https://github.com/hivecommons/crux): starts AI coding agent CLIs in tmux from a YAML definition, sends them work, and restarts or pauses agents that stall. Latest release v0.2.0 (early software). Docs: https://docs.hivecommons.dev/docs/crux/overview/introduction
- [Spektacular (Spek)](https://spektacular.dev): spec-driven development for AI coding agents; a markdown spek becomes a reviewed plan and an agent-driven implementation.
- [hotshot](https://github.com/hivecommons/hotshot): watches for new screenshots and injects them into the terminal session you last used, for AI coding assistants that accept image paths.
- [pluk](https://github.com/hivecommons/pluk): lightweight pub-sub event streaming for tmux sessions running AI coding agents; classifies, subscribes to, and reacts to their JSONL event streams.
diff --git a/scripts/llms-txt.test.mjs b/scripts/llms-txt.test.mjs
index 9406621..0f3f027 100644
--- a/scripts/llms-txt.test.mjs
+++ b/scripts/llms-txt.test.mjs
@@ -127,6 +127,20 @@ export function projectCards(html) {
return cards;
}
+// --- version gates -----------------------------------------------------------
+
+// Extract the Crux version tag from the project carousel: "
Crux ... vX.Y.Z
"
+export function cruxVersionFromHtml(html) {
+ const m = html.match(/
Crux\s*]*>([^<]*)<\/span><\/h3>/);
+ return m ? m[1] : null;
+}
+
+// Extract the Crux version from llms.txt: "First release vX.Y.Z" or "Latest release vX.Y.Z"
+export function cruxVersionFromLlms(text) {
+ const m = text.match(/\[Crux\].*?(?:First|Latest) release (v\d+\.\d+\.\d+)/);
+ return m ? m[1] : null;
+}
+
function setDiff(a, b) {
const sb = new Set(b);
return a.filter((x) => !sb.has(x));
@@ -265,6 +279,22 @@ test("projectCards reads each carousel card's badge-free name from anchor and di
assert.deepEqual(projectCards("nothing
"), []);
});
+test("fixture: cruxVersionFromHtml extracts version from Crux carousel tag or returns null when absent", () => {
+ assert.equal(cruxVersionFromHtml('Crux v0.2.0
'), "v0.2.0");
+ assert.equal(cruxVersionFromHtml('Crux v1.0.0
'), "v1.0.0");
+ assert.equal(cruxVersionFromHtml("Other v1.0.0
"), null);
+ assert.equal(cruxVersionFromHtml("Crux
"), null);
+ assert.equal(cruxVersionFromHtml(""), null);
+});
+
+test("fixture: cruxVersionFromLlms extracts version from llms.txt or returns null when absent", () => {
+ assert.equal(cruxVersionFromLlms("- [Crux](https://github.com/hivecommons/crux): ... Latest release v0.2.0 (early software)."), "v0.2.0");
+ assert.equal(cruxVersionFromLlms("- [Crux](https://github.com/hivecommons/crux): ... First release v0.1.0 (early software)."), "v0.1.0");
+ assert.equal(cruxVersionFromLlms("- [Other](https://github.com/hivecommons/other): ... Latest release v1.0.0."), null);
+ assert.equal(cruxVersionFromLlms("- [Crux](https://github.com/hivecommons/crux): ... No version here."), null);
+ assert.equal(cruxVersionFromLlms(""), null);
+});
+
// --- live checks -------------------------------------------------------------
const live = parseLlmsTxt(LLMS_TXT);
@@ -320,3 +350,16 @@ test("llms.txt Projects list names the project carousel cards on index.html, in
"llms.txt '## Projects' must list the same projects as the index.html carousel, in carousel order",
);
});
+
+test("Crux version in llms.txt matches the carousel tag in index.html and both are valid semver", () => {
+ const htmlVersion = cruxVersionFromHtml(INDEX_HTML);
+ const llmsVersion = cruxVersionFromLlms(LLMS_TXT);
+ const semverPattern = /^v\d+\.\d+\.\d+$/;
+
+ assert.ok(htmlVersion, "Crux version tag not found in index.html carousel");
+ assert.ok(llmsVersion, "Crux version not found in llms.txt");
+ assert.match(htmlVersion, semverPattern, `index.html Crux tag '${htmlVersion}' is not valid semver`);
+ assert.match(llmsVersion, semverPattern, `llms.txt Crux version '${llmsVersion}' is not valid semver`);
+ assert.equal(htmlVersion, llmsVersion, `Crux version mismatch: index.html has '${htmlVersion}' but llms.txt has '${llmsVersion}' — keep them in sync`);
+});
+