Skip to content
Open
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
85 changes: 53 additions & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
# Continuum CI — TypeScript clients gate
#
# ## What this replaced, and why it mattered
#
# This workflow used to run in `working-directory: src` against
# `src/package-lock.json`, calling `npm run build:ts` and `npm run test:crud`.
# Every one of those is gone: `src/` was the Node monolith, retired when the
# substrate became a headless Rust core (#1840), and both scripts left with it.
#
# So it failed on every run — last success 2026-06-07, red continuously from
# 2026-06-09 — with `Cannot find module 'dotenv'`, an error that describes the
# absence of the whole world it was pointed at rather than any defect in the
# code under test.
#
# A check that ALWAYS fails is worse than no check. It cannot distinguish a
# broken PR from a healthy one, so the only thing anyone can learn from it is to
# stop reading CI — which is exactly what happened: work routed around it
# through the Rust and drift-guard workflows for two months.
#
# ## What it does now
#
# Gates the TypeScript clients workspace — the one thing this file was always
# supposed to cover and had stopped being able to see.
#
# `npm run test:clients` was ALREADY in package.json and ALREADY passing
# locally. Nothing called it. That is how `renderBench.spec.ts` and six sibling
# spec files across `apps/web` and `packages/` came to exist, be green on a
# developer's machine, and gate nothing at all — a correct check that nothing
# invokes, which is indistinguishable from having no check until someone greps
# for the caller.
#
# Triggers on `canary` as well as `main`, because canary is where development
# happens; a gate that only watches the stable line learns about breakage after
# it has already been merged.

name: Continuum CI

on:
push:
branches: [ main ]
branches: [ main, canary ]
pull_request:
branches: [ main ]
branches: [ main, canary ]

jobs:
validate:
clients:
name: TypeScript clients (typecheck + tests)
runs-on: ubuntu-latest
defaults:
run:
working-directory: src

steps:
- uses: actions/checkout@v4
Expand All @@ -21,32 +54,20 @@ jobs:
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: src/package-lock.json
# Root lockfile — npm workspaces hoist, so there is exactly one.
cache-dependency-path: package-lock.json

- name: Install dependencies
# `npm ci` installs the whole workspace from the lockfile, which is what
# makes this job the substrate the tests stand on: without it, every spec
# file fails at COLLECTION with "Failed to load url @continuum/chat-view",
# an error that points a reader at missing source rather than missing deps.
- name: Install workspace
run: npm ci

- name: TypeScript compilation
run: |
npm run build:ts
echo "✅ TypeScript compilation passed"

# Skip full tests for documentation-only PRs
- name: Check if documentation-only PR
id: check_pr
working-directory: .
run: |
if git diff --name-only origin/main..HEAD | grep -qvE '\.(md|txt|yml|yaml)$'; then
echo "skip_tests=false" >> $GITHUB_OUTPUT
else
echo "skip_tests=true" >> $GITHUB_OUTPUT
fi

- name: Run tests
if: steps.check_pr.outputs.skip_tests != 'true'
run: |
npm run test:crud
echo "✅ CRUD tests passed"

- name: Validation complete
run: echo "✅ CI validation complete - local precommit hook validates full system"
- name: Typecheck clients
run: npm run typecheck:clients

# The suite that existed and was never run. Covers apps/web, apps/tui,
# the view packages, and the SDK.
- name: Test clients
run: npm run test:clients
58 changes: 58 additions & 0 deletions .github/workflows/plugin-version-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Plugin version-bump guard.
#
# Why this exists: Claude Code plugin updates are VERSION-based, not
# content-based. A marketplace install copies the plugin to
# ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/ and pins it. `claude
# plugin update` compares the DECLARED version in plugin.json against the
# installed one and never looks at the files. So if scripts change and the
# version does not, every installed copy answers "already at the latest version"
# forever and the fix reaches nobody. `git pull` does not update a plugin.
#
# The failure this guards, measured 2026-08-09: memory-bridge sat at 0.1.0 since
# 2026-07-25 while its scripts gained a persona-id cache and an entire
# session-capture.sh. The installed copy on BigMama had NEITHER — automatic
# per-turn memory capture had never run once on that machine — while the repo
# held working code and the plugin README said the bridge was live. Bumping
# 0.1.0 -> 0.2.0 propagated two weeks of fixes in one command.
#
# Same class as the install-manifest projection guard and the ts-rs binding
# guard: a consumed artifact and its source must not drift apart in silence.
# Here the "artifact" is every developer's installed copy.
#
# NOTE: this runs in CI, not pre-commit. `.githooks/pre-commit` currently invokes
# tests deleted with the Node monolith and is not installed as the active hook
# (core.hooksPath does not point at it), so wiring a gate there would look
# enforced while running never.
name: Plugin Version Guard

on:
pull_request:
paths:
- 'tools/plugins/**'
- '.github/workflows/plugin-version-guard.yml'
push:
branches: [canary, main]

concurrency:
group: plugin-version-${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
plugin-version:
name: plugin content changed => version bumped
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
# Need the base commit to diff against, not just the tip.
fetch-depth: 0

- name: Check every touched plugin bumped its version
run: |
BASE="${{ github.event.pull_request.base.sha || github.event.before }}"
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then
echo "no usable base ref (first push / new branch) — nothing to compare"
exit 0
fi
tools/scripts/check-plugin-version.sh "$BASE"
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"test": "TZ=UTC vitest run",
"test:watch": "TZ=UTC vitest"
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@continuum/chat-view": "*",
Expand Down
72 changes: 72 additions & 0 deletions core/continuum-core/src/bin/continuum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ async fn run() -> Result<(), String> {
eprintln!("{}", usage());
Ok(())
}
// Handled HERE, never dispatched. `version` asks what THIS BINARY is;
// forwarding it to the core answered a different question and, when no
// core was running, answered none at all — the operator asking "what am
// I holding?" got "the substrate refused your command."
"version" | "--version" | "-V" => {
println!("{}", version_line());
Ok(())
}
"start" => {
// Collect once: `args.any(..)` consumes the iterator, so reading a
// second flag off it afterwards would silently always be false.
Expand Down Expand Up @@ -2137,7 +2145,71 @@ mod tests {
}
}

/// The name this binary was actually INVOKED as, for help text.
///
/// One binary ships under several names — `uu` (the short canonical one) and
/// `continuum` (the long-form alias kept so existing scripts and docs keep
/// working). Hardcoding "continuum" in the usage text meant `uu --help` printed
/// `usage: continuum ...`: the front door did not know its own name, and every
/// example it gave was a command the reader had not typed.
///
/// Derived from argv[0] rather than a constant so a new alias is correct the
/// moment it exists, with nothing to remember to update. Falls back to the
/// canonical name when argv[0] is missing or unreadable — an odd exec is not a
/// reason to print nothing.
fn program_name() -> String {
std::env::args_os()
.next()
.map(std::path::PathBuf::from)
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "uu".to_string())
}

/// This binary's build identity — number, sha, and when it was compiled.
///
/// Joel's ruling 2026-08-08: versions must ALWAYS auto-increment and display
/// with the sha, in EVERY repo, visible on connection/health/query, because
/// stale binaries have repeatedly poisoned testing.
///
/// This was UNANSWERABLE from the front door before: `continuum version` fell
/// through to the substrate dispatcher and came back
/// `Unknown command: 'version'` — the CLI could ask the core what IT was and
/// could not say what ITSELF was. That is the exact gap that lets an operator
/// debug a fixed bug with an unfixed binary in their hand.
fn version_line() -> String {
format!(
"{} #{} {} built {}",
program_name(),
option_env!("CONTINUUM_BUILD_NUMBER").unwrap_or("0"),
option_env!("CONTINUUM_BUILD_GIT_SHA").unwrap_or("unknown"),
option_env!("CONTINUUM_BUILD_AT").unwrap_or("unknown"),
)
}

fn usage() -> String {
let me = program_name();
format!(
"usage: {me} <start|reboot|stop|version|command> [json | --key value ...]\n\
\n\
Lifecycle:\n \
{me} start build + run the headless Rust core (detached), wait until ready\n \
{me} reboot rebuild + relaunch, replacing any running core (~0 downtime)\n \
{me} stop stop the running core\n \
{me} version this binary's build number + sha (NOT the core's)\n\
\n\
Commands (dispatch to the running core):\n \
{me} ping\n \
{me} ping --message hi # --key value, coerced + camelCased automatically\n \
{me} commands/list # discover commands dynamically (single source)\n\
\n\
Env: CONTINUUM_CORE_SOCKET (default /tmp/continuum-core.sock)\n \
CONTINUUM_START_SCRIPT (override the start script path)"
)
}

#[allow(dead_code)]
fn usage_legacy() -> String {
"usage: continuum <start|reboot|stop|command> [json | --key value ...]\n\
\n\
Lifecycle:\n \
Expand Down
Loading
Loading