Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions .changeset/tidy-tigers-detach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"cotal-ai": minor
"@cotal-ai/cli": minor
"@cotal-ai/connector-core": minor
"@cotal-ai/workspace": minor
---

Make `cotal up` detach by default and keep a durable self-hosted mesh record so stopped stacks remain discoverable and restartable from their recorded root.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Prefer your agent to do it? Point it at <https://docs.cotal.ai/prompt.md>.
Setup gets your machine ready and **starts nothing**. Then:

```bash
cotal up --detach # start the mesh
cotal up # start the detached mesh
cotal spawn # put your agent on it and talk to it (Ctrl-C to leave)
cotal web # watch it in the browser
cotal down # stop everything
Expand Down
3 changes: 3 additions & 0 deletions bin/smoke/ci-suites.txt
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,8 @@ smoke:claude-boot-wake
# Control commands resolve ws:// / wss:// meshes too. Exercise the real CLI against one broker's
# websocket and TCP listeners so raw node-transport regressions name their `wsconnect` refusal.
smoke:control-transport-dial
# Windows detached stacks must either escape the invoking job with CREATE_BREAKAWAY_FROM_JOB or
# fail before launch and point to --foreground. Pure branch coverage runs on every CI platform.
# A manager restart killed mid-barrier left the issuance gate frozen, and the successor refused
# SPEC 13.8 until an operator ran reconcile-gate (#783 item 3 / #871). Boot now completes that
# same dead registration when the freeze-holder is gone under a complete CONNZ sweep. Appended
Expand All @@ -543,3 +545,4 @@ smoke:web-console-auth
smoke:no-implicit-general
smoke:session-channels
smoke:jcode-private-lifecycle
smoke:windows-detached-spawn
2 changes: 1 addition & 1 deletion bin/smoke/flag-inventory.smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const GOLDEN: Record<string, { flags: string[]; positionals: boolean; rawArgs?:
update: { flags: ["self:boolean"], positionals: false },
up: {
flags: [
"channels:string", "detach:boolean", "dry-run:boolean", "file:string:f", "host:string",
"channels:string", "detach:boolean", "dry-run:boolean", "file:string:f", "foreground:boolean", "host:string",
"idp:string", "open:boolean", "runtime:string", "server:string", "space:string",
// The optional PUBLIC remote-exchange face, threaded to the auth-service daemon.
// `--advertised-server` (2026-08): with --exchange-public-port, the broker address the public
Expand Down
23 changes: 23 additions & 0 deletions bin/smoke/mutations/claim-space-live-root.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"suite": "implementations/cli/smoke/meshes-registry.smoke.ts",
"guard": "a self-hosted mesh may move to a new server on the same canonical root only after its recorded endpoint is dead",
"command": "set +u; unset ${!COTAL_@}; TMPDIR=\"$(node -e 'process.stdout.write(require(\"node:fs\").realpathSync.native(require(\"node:os\").tmpdir()))')\" pnpm smoke:meshes-registry",
"progressPattern": "^ ✓",
"minTicks": 1,
"proveWith": "node scripts/mutation-proof.mjs --config bin/smoke/mutations/claim-space-live-root.json",
"why": [
"The suite owns its open nats-server on a throwaway port and redirects the registry through COTAL_HOME before imports.",
"The mutation removes only the liveness refusal from the same-root self-hosted branch. The live competing-broker cell must red, while the dead prior-endpoint auto-port restart control remains green."
],
"mutations": [
{
"name": "same-root self-hosted claim skips the recorded endpoint liveness probe",
"file": "implementations/cli/src/commands/up.ts",
"find": " if (canonicalRoot(existing.root) === canonicalRoot(root)) {\n if (await isReachable(existing.server))\n throw new Error(`space \"${space}\" is already in use by a mesh at ${existing.server} (${existing.root}) - pick a different \\`--space\\`, or \\`cotal down\\` it first`);\n return;\n }",
"replace": " if (canonicalRoot(existing.root) === canonicalRoot(root)) return;",
"expectRed": "a same-root self-hosted record at a LIVE different endpoint blocks a competing broker",
"cell": "a same-root self-hosted record at a LIVE different endpoint blocks a competing broker",
"note": "Restores the unsafe early return while leaving the dead-endpoint restart path unchanged."
}
]
}
34 changes: 34 additions & 0 deletions bin/smoke/mutations/up-daemonize-persist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"suite": "bin/smoke/up-stack-live.smoke.ts",
"guard": "bare cotal up persists a durable self-hosted record and launches a process-independent stack",
"command": "pnpm build && env -i PATH=$PATH HOME=$HOME TMPDIR=$TMPDIR USER=$USER SHELL=$SHELL pnpm smoke:up-stack:live",
"proveWith": "node scripts/mutation-proof.mjs --config bin/smoke/mutations/up-daemonize-persist.json",
"why": [
"This is a real-broker end-to-end suite through the published CLI composition root. The command rebuilds first because package resolution reaches product code through dist.",
"No completionMarker: the suite is fail-fast (ok() throws) and its success banner is the one line a killed run never reaches. Declaring UP-STACK LIVE SMOKE OK would invert every genuine kill into INCONCLUSIVE.",
"Mutation 1 removes the provision-time registry write. The named persist cell is the first record-dependent check and uses existsSync, so a missing file reddens that cell instead of throwing ENOENT on a later auto-port read.",
"Mutation 2 restores the old opt-in detach decision. Bare up then enters the foreground path, which prints Ctrl-C copy and never emits the detached banner; spawnSync SIGTERM of that path can still exit 0, so the named cell requires the banner and the absence of Ctrl-C. afterRestore rebuilds because dist/ is gitignored."
],
"mutations": [
{
"name": "provisioning skips the durable mesh record",
"file": "implementations/cli/src/commands/up.ts",
"find": " recordOurMesh({\n space, server, root: cotalRoot(),",
"replace": " if (false) recordOurMesh({\n space, server, root: cotalRoot(),",
"expectRed": "up persists a self-hosted mesh record at provision time",
"cell": "up persists a self-hosted mesh record at provision time",
"note": "Deletes the effective persistence call while leaving launch intact.",
"afterRestore": "pnpm build"
},
{
"name": "detach returns to opt-in instead of being the default",
"file": "implementations/cli/src/commands/up.ts",
"find": " if (!values.foreground) {",
"replace": " if (values.detach) {",
"expectRed": "bare up exits 0 while the stack remains alive (detached by default)",
"cell": "bare up exits 0 while the stack remains alive (detached by default)",
"note": "Restores the old behavior at the ordinary up launch branch.",
"afterRestore": "pnpm build"
}
]
}
Loading
Loading