diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl
index ead6db3..f2c75f6 100644
--- a/.beads/issues.jsonl
+++ b/.beads/issues.jsonl
@@ -1,3 +1,4 @@
+{"_type":"issue","id":"islandflow-k4f","title":"Gate deploy script on docker workspace snapshot sync","description":"Prevent frozen-lockfile build failures during deploy by adding a local preflight in scripts/deploy.ts that runs bun run check:docker-workspace and aborts with a clear sync+commit remediation message when stale.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:01:44Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:04:11Z","started_at":"2026-05-15T23:01:48Z","closed_at":"2026-05-15T23:04:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-xll","title":"Fix bun.lock drift causing frozen-lockfile Docker build failures","description":"Docker image builds fail in multiple targets (candles, web, ingest services) because bun install --frozen-lockfile detects lockfile changes. Update workspace lockfile to match manifests and verify frozen install succeeds.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T22:52:38Z","created_by":"dirtydishes","updated_at":"2026-05-15T22:55:23Z","started_at":"2026-05-15T22:52:40Z","closed_at":"2026-05-15T22:55:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-9nd","title":"Hosted synthetic tape redesign with internal control surface","description":"Implement hosted synthetic market redesign with shared deterministic regime engine, internal JetStream KV control plane, ingest coupling across options and equities, and an internal bottom-right synthetic-control drawer with Next proxy routes. Preserve the six public smart-money categories while adding hidden subtype families, soft coverage accounting, and backend-only admin endpoints.\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-14T01:25:02Z","created_by":"dirtydishes","updated_at":"2026-05-14T02:10:03Z","started_at":"2026-05-14T01:25:09Z","closed_at":"2026-05-14T02:10:03Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"islandflow-9dz","title":"Tune synthetic smart-money scenario coverage","description":"Redesign synthetic smart-money option prints so the emitted scenarios trigger each classifier category more consistently while staying directionally plausible. Focus on scenario mix, DTE/moneyness, price placement, and event/structure context so the Electron demo reliably shows institutional directional, retail whale, event-driven, vol seller, arbitrage, and hedge reactive hits.\n","status":"in_progress","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-13T21:36:37Z","created_by":"dirtydishes","updated_at":"2026-05-13T21:36:41Z","started_at":"2026-05-13T21:36:41Z","dependency_count":0,"dependent_count":0,"comment_count":0}
diff --git a/bun.lock b/bun.lock
index c660953..46160a7 100644
--- a/bun.lock
+++ b/bun.lock
@@ -39,6 +39,7 @@
"packages/bus": {
"name": "@islandflow/bus",
"dependencies": {
+ "@islandflow/types": "workspace:*",
"nats": "^2.24.0",
},
},
diff --git a/deployment/docker/workspace-root/bun.lock b/deployment/docker/workspace-root/bun.lock
index c660953..46160a7 100644
--- a/deployment/docker/workspace-root/bun.lock
+++ b/deployment/docker/workspace-root/bun.lock
@@ -39,6 +39,7 @@
"packages/bus": {
"name": "@islandflow/bus",
"dependencies": {
+ "@islandflow/types": "workspace:*",
"nats": "^2.24.0",
},
},
diff --git a/docs/turns/2026-05-15-deploy-preflight-docker-workspace-check.html b/docs/turns/2026-05-15-deploy-preflight-docker-workspace-check.html
new file mode 100644
index 0000000..fbeb67d
--- /dev/null
+++ b/docs/turns/2026-05-15-deploy-preflight-docker-workspace-check.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+ Turn Report - 2026-05-15 - Deploy preflight docker workspace check
+
+
+
+ Turn Report: Deploy script preflight guard for Docker workspace snapshot
+ Date/Time: 2026-05-15 19:03:09 EDT
+
+ Summary
+
+ Updated scripts/deploy.ts so ./deploy now fails fast when
+ deployment/docker/workspace-root is stale. The script now runs
+ bun run check:docker-workspace during local prechecks and prints a clear remediation
+ message to run sync + commit before deployment.
+
+
+ Changes Made
+
+ - Created
localWorkspaceSnapshotPrecheck() in scripts/deploy.ts.
+ - Added preflight invocation to both deployment modes:
+
+ localMainPrecheck()
+ localBranchPrecheck()
+
+
+ - On failure, deploy now exits with an explicit message:
+
+ Refusing deploy: deployment/docker/workspace-root is out of sync.
+Run bun run sync:docker-workspace, commit updated snapshot files, then retry deploy.
+
+ - Refreshed lock state to keep checks green:
+
+ bun.lock
+ deployment/docker/workspace-root/bun.lock
+
+
+
+
+ Context
+
+ The deployment compose stack builds from a snapshot under
+ deployment/docker/workspace-root. If that snapshot drifts from the active
+ workspace graph, Docker build-time bun install --frozen-lockfile fails remotely.
+ This change catches drift locally before any remote rollout starts.
+
+
+ Important Implementation Details
+
+ - Preflight uses
spawnSync("bun", ["run", "check:docker-workspace"]) with inherited stdio for transparent output.
+ - Failure exits with the same non-zero status, preserving script CI/shell behavior.
+ - Guard applies to both
./deploy main and ./deploy current-branch flows.
+
+
+ Validation
+
+ - Passed:
bun run scripts/deploy.ts --help
+ - Passed:
bun run check:docker-workspace (after lock sync)
+
+
+ Issues, Limitations, and Mitigations
+
+ - Limitation: Did not execute a full remote deploy during this turn.
+ - Mitigation: The guard is in the local precheck path, so next real deploy run will enforce the new check automatically.
+
+
+ Follow-up Work
+
+ - Optional defense-in-depth: run
bun run check:docker-workspace on the server in remote rollout before docker compose up -d --build.
+ - Optional CI gate: add
bun run check:docker-workspace to PR checks to prevent stale snapshots reaching main.
+ - Beads issue:
islandflow-k4f.
+
+
+
diff --git a/scripts/deploy.ts b/scripts/deploy.ts
index 87abd52..b76a393 100644
--- a/scripts/deploy.ts
+++ b/scripts/deploy.ts
@@ -11,16 +11,32 @@ const REMOTE_HOST = "delta@152.53.80.229";
const REMOTE_REPO = "/home/delta/islandflow";
const REMOTE_DEPLOYMENT = "/home/delta/islandflow/deployment/docker";
const SSH_KEY = path.join(process.env.HOME ?? "", ".ssh", "delta_ed25519");
-const SSH_OPTIONS = ["-i", SSH_KEY, "-o", "IdentitiesOnly=yes", "-o", "BatchMode=yes"];
+const SSH_OPTIONS = [
+ "-i",
+ SSH_KEY,
+ "-o",
+ "IdentitiesOnly=yes",
+ "-o",
+ "BatchMode=yes",
+];
const ALLOWED_REMOTE_UNTRACKED = new Set([
"deployment/docker/signal-cli-0.14.3-Linux-native.tar.gz",
- "deployment/npm/"
+ "deployment/npm/",
]);
const API_CONTAINER = "islandflow-vps-api-1";
const WEB_CONTAINER = "islandflow-vps-web-1";
-const PUBLIC_APP_URL = process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io";
-const PUBLIC_API_HEALTH_URL = process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null;
-const LOG_SERVICES = ["api", "web", "compute", "candles", "ingest-options", "ingest-equities"];
+const PUBLIC_APP_URL =
+ process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io";
+const PUBLIC_API_HEALTH_URL =
+ process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null;
+const LOG_SERVICES = [
+ "api",
+ "web",
+ "compute",
+ "candles",
+ "ingest-options",
+ "ingest-equities",
+];
const scriptPath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(scriptPath), "..");
@@ -55,12 +71,16 @@ function formatCommand(command: string, args: string[]): string {
.join(" ");
}
-function runChecked(command: string, args: string[], options: SpawnSyncOptions = {}): void {
+function runChecked(
+ command: string,
+ args: string[],
+ options: SpawnSyncOptions = {},
+): void {
console.log(`$ ${formatCommand(command, args)}`);
const result = spawnSync(command, args, {
cwd: repoRoot,
stdio: "inherit",
- ...options
+ ...options,
});
if (result.status !== 0) {
@@ -68,12 +88,16 @@ function runChecked(command: string, args: string[], options: SpawnSyncOptions =
}
}
-function captureChecked(command: string, args: string[], options: SpawnSyncOptions = {}): string {
+function captureChecked(
+ command: string,
+ args: string[],
+ options: SpawnSyncOptions = {},
+): string {
const result = spawnSync(command, args, {
cwd: repoRoot,
encoding: "utf8",
stdio: ["inherit", "pipe", "pipe"],
- ...options
+ ...options,
});
if (result.status !== 0) {
@@ -84,7 +108,11 @@ function captureChecked(command: string, args: string[], options: SpawnSyncOptio
return result.stdout ?? "";
}
-function runRemoteScript(title: string, script: string, args: string[] = []): void {
+function runRemoteScript(
+ title: string,
+ script: string,
+ args: string[] = [],
+): void {
section(title);
const sshArgs = [...SSH_OPTIONS, REMOTE_HOST, "bash", "-s", "--", ...args];
console.log(`$ ${formatCommand("ssh", sshArgs)}`);
@@ -92,7 +120,7 @@ function runRemoteScript(title: string, script: string, args: string[] = []): vo
cwd: repoRoot,
input: script,
encoding: "utf8",
- stdio: ["pipe", "inherit", "inherit"]
+ stdio: ["pipe", "inherit", "inherit"],
});
if (result.status !== 0) {
@@ -100,7 +128,10 @@ function runRemoteScript(title: string, script: string, args: string[] = []): vo
}
}
-function parseArgs(rawArgs: string[]): { mode: DeployMode; forceRecreate: boolean } {
+function parseArgs(rawArgs: string[]): {
+ mode: DeployMode;
+ forceRecreate: boolean;
+} {
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
usage(0);
}
@@ -114,7 +145,9 @@ function parseArgs(rawArgs: string[]): { mode: DeployMode; forceRecreate: boolea
if (
(positional.length === 1 && positional[0] === "current-branch") ||
- (positional.length === 2 && positional[0] === "current" && positional[1] === "branch")
+ (positional.length === 2 &&
+ positional[0] === "current" &&
+ positional[1] === "branch")
) {
return { mode: "current-branch", forceRecreate };
}
@@ -129,12 +162,28 @@ function assertSshKeyExists(): void {
}
}
+function localWorkspaceSnapshotPrecheck(): void {
+ console.log("$ bun run check:docker-workspace");
+ const result = spawnSync("bun", ["run", "check:docker-workspace"], {
+ cwd: repoRoot,
+ stdio: "inherit",
+ });
+
+ if (result.status !== 0) {
+ console.error(
+ "Refusing deploy: deployment/docker/workspace-root is out of sync. Run `bun run sync:docker-workspace`, commit updated snapshot files, then retry deploy.",
+ );
+ process.exit(result.status ?? 1);
+ }
+}
+
function localMainPrecheck(): void {
section("Local Precheck");
runChecked("git", ["fetch", "origin"]);
runChecked("git", ["status", "--short", "--branch"]);
runChecked("git", ["rev-parse", "--verify", "HEAD"]);
runChecked("git", ["rev-parse", "origin/main"]);
+ localWorkspaceSnapshotPrecheck();
}
function currentBranchName(): string {
@@ -155,10 +204,12 @@ function localBranchPrecheck(branch: string): void {
const porcelain = captureChecked("git", ["status", "--porcelain=v1"]).trim();
if (porcelain) {
console.error(
- `Refusing to deploy ${branch} with uncommitted local changes. Commit the intended state first.`
+ `Refusing to deploy ${branch} with uncommitted local changes. Commit the intended state first.`,
);
process.exit(1);
}
+
+ localWorkspaceSnapshotPrecheck();
}
function publishCurrentBranch(branch: string): void {
@@ -169,8 +220,8 @@ function publishCurrentBranch(branch: string): void {
{
cwd: repoRoot,
encoding: "utf8",
- stdio: ["inherit", "pipe", "pipe"]
- }
+ stdio: ["inherit", "pipe", "pipe"],
+ },
);
if (upstreamResult.status === 0) {
@@ -218,12 +269,18 @@ while IFS= read -r line; do
;;
esac
done <<< "$status"
-`
+`,
);
}
-function remoteRollout(mode: DeployMode, branch: string | null, forceRecreate: boolean): void {
- const composeArgs = forceRecreate ? "up -d --build --force-recreate" : "up -d --build";
+function remoteRollout(
+ mode: DeployMode,
+ branch: string | null,
+ forceRecreate: boolean,
+): void {
+ const composeArgs = forceRecreate
+ ? "up -d --build --force-recreate"
+ : "up -d --build";
const switchCommand =
mode === "main"
? `git switch main
@@ -242,7 +299,7 @@ ${switchCommand}
cd "${REMOTE_DEPLOYMENT}"
docker compose ${composeArgs}
-`
+`,
);
}
@@ -257,7 +314,7 @@ docker compose ps
docker compose logs --tail=100 ${LOG_SERVICES.join(" ")}
docker exec ${API_CONTAINER} bun -e 'const r = await fetch("http://127.0.0.1:4000/health"); console.log(await r.text())'
docker exec ${WEB_CONTAINER} bun -e 'const r = await fetch("http://127.0.0.1:3000/"); console.log(r.status)'
-`
+`,
);
}
@@ -271,7 +328,7 @@ function publicVerification(): void {
}
console.log(
- "Skipping separate public API health check; same-origin mode relies on the public app check plus container-local API verification."
+ "Skipping separate public API health check; same-origin mode relies on the public app check plus container-local API verification.",
);
}
@@ -293,7 +350,7 @@ function main(): void {
console.log(
mode === "main"
? "Deploying origin/main to the existing Islandflow VPS checkout."
- : "Deploying the current local branch to the existing Islandflow VPS checkout."
+ : "Deploying the current local branch to the existing Islandflow VPS checkout.",
);
if (mode === "main") {