## Summary Add an opt-in source-development loop that restarts the dashboard and engine when runtime TypeScript or JSON changes. Use `pnpm dev:watch`; `pnpm dev:hmr` now combines Vite UI HMR with the same supervised API/engine restart path. The watcher filters tests, fixtures, generated declarations, build output, and task state. It coalesces bursts with a two-second maximum wait, waits for the child to acknowledge its IPC listener, and rebuilds runtime dist artifacts before a source-triggered respawn. ## Safety model - Close scheduler, triage, heartbeat, mission, routine, self-healing, and merge admission before checking for active work. - Let already-running agents reach a safe boundary; do not mutate durable pause settings. - Enter the existing graceful exit-code-86 shutdown and supervised respawn path. - Retry failed liveness reads and declined restart requests instead of dropping the pending change. - Keep ordinary `pnpm dev` behavior unchanged; inherited watch state does not break nested non-dashboard development commands. A development restart intentionally replaces the dashboard process, so transient dashboard connections and project dev-server children reconnect or restart with it. Agent work is the protected boundary. ## Validation - `pnpm lint` - `pnpm test:gate` (753 tests passed across engine, core, PostgreSQL gate, and CI-shape suites) - Focused CLI watcher/restart/supervision suites: 40 tests passed - Focused engine drain/manager suites: 52 tests passed - `pnpm --filter @runfusion/fusion typecheck` - `pnpm --filter @fusion/engine typecheck` - `pnpm verify:fast` (13 steps passed, including CLI build and real health boot smoke) - Manual unsupported-command probe confirms explicit `--watch` fails clearly outside the dashboard command ## Post-Deploy Monitoring & Validation - Watch for `[fusion:dev] source changed`, `source restart deferred`, `active work drained`, and `restart requested` logs during the first watched development session. - Healthy behavior is one exit-86 respawn per edit batch, no interrupted active agents, refreshed dist artifacts, and a healthy dashboard after respawn. - Investigate repeated restart loops, watcher attachment warnings, declined restart retries, or liveness-read failures. - Immediate mitigation is to use ordinary `pnpm dev` without `--watch`; no production runtime behavior or durable setting needs rollback. - Validation owner: Fusion maintainers during the first source edit after merge. --- [](https://github.com/EveryInc/compound-engineering-plugin) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added `pnpm dev:watch` to automatically restart development runtime processes when source files change. * Development restarts now wait for active work to finish, preventing new work from starting during the transition. * Enhanced `pnpm dev:hmr` with graceful runtime source restarts while keeping the dashboard available. * Rapid source changes are grouped to avoid unnecessary restarts. * **Documentation** * Updated development setup and contribution guides with the new watch workflow. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
import { watch as fsWatch } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
export const DEFAULT_DEV_SOURCE_WATCH_PATHS = [
|
|
"packages/core/src",
|
|
"packages/engine/src",
|
|
"packages/dashboard/src",
|
|
"packages/cli/src",
|
|
];
|
|
|
|
const RUNTIME_SOURCE_EXTENSION = /\.(?:[cm]?[jt]sx?|json)$/i;
|
|
const NON_RUNTIME_SEGMENTS = new Set([
|
|
"__fixtures__",
|
|
"__generated__",
|
|
"__tests__",
|
|
"fixtures",
|
|
"generated",
|
|
"test",
|
|
"tests",
|
|
]);
|
|
|
|
export function isRestartableSourceFile(filename) {
|
|
if (filename === null || filename === undefined) return false;
|
|
const normalized = String(filename).replaceAll("\\", "/");
|
|
if (!normalized || !RUNTIME_SOURCE_EXTENSION.test(normalized)) return false;
|
|
if (/\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(normalized)) return false;
|
|
if (/\.d\.[cm]?ts$/i.test(normalized)) return false;
|
|
return !normalized.split("/").some((segment) => NON_RUNTIME_SEGMENTS.has(segment));
|
|
}
|
|
|
|
/**
|
|
* FNXC:DevEngineWatch 2026-08-04-01:25:
|
|
* Watch only source roots that execute inside the long-lived dashboard/engine
|
|
* process. Tests, fixtures, generated declarations, build output, task state,
|
|
* and worktrees must not create restart storms. The supervisor owns process
|
|
* replacement; this helper only coalesces source events and reports paths.
|
|
*/
|
|
export function createDevSourceWatcher({
|
|
rootDir,
|
|
onRestart,
|
|
watchPaths = DEFAULT_DEV_SOURCE_WATCH_PATHS,
|
|
debounceMs = 350,
|
|
maxWaitMs = 2_000,
|
|
watch = fsWatch,
|
|
logger = console,
|
|
}) {
|
|
const watchers = [];
|
|
const watchedPaths = [];
|
|
const changedPaths = new Set();
|
|
let debounceTimer;
|
|
let firstChangeAt;
|
|
let closed = false;
|
|
|
|
const scheduleRestart = (watchPath, filename) => {
|
|
if (closed || !isRestartableSourceFile(filename)) return;
|
|
const normalized = String(filename).replaceAll("\\", "/");
|
|
changedPaths.add(`${watchPath}/${normalized}`);
|
|
firstChangeAt ??= Date.now();
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
const elapsedMs = Date.now() - firstChangeAt;
|
|
const delayMs = Math.min(debounceMs, Math.max(0, maxWaitMs - elapsedMs));
|
|
debounceTimer = setTimeout(() => {
|
|
debounceTimer = undefined;
|
|
firstChangeAt = undefined;
|
|
const paths = [...changedPaths];
|
|
changedPaths.clear();
|
|
Promise.resolve(onRestart(paths)).catch((error) => {
|
|
logger.warn(`[fusion:dev] source restart request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
});
|
|
}, delayMs);
|
|
debounceTimer.unref?.();
|
|
};
|
|
|
|
for (const watchPath of watchPaths) {
|
|
const absolutePath = join(rootDir, watchPath);
|
|
try {
|
|
const watcher = watch(absolutePath, { recursive: true }, (_eventType, filename) => {
|
|
scheduleRestart(watchPath, filename);
|
|
});
|
|
watcher.on?.("error", (error) => {
|
|
logger.warn(`[fusion:dev] source watcher error for ${watchPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
});
|
|
watchers.push(watcher);
|
|
watchedPaths.push(watchPath);
|
|
} catch (error) {
|
|
logger.warn(`[fusion:dev] could not watch ${watchPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
watchedPaths,
|
|
close() {
|
|
closed = true;
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
debounceTimer = undefined;
|
|
firstChangeAt = undefined;
|
|
changedPaths.clear();
|
|
for (const watcher of watchers) {
|
|
try {
|
|
watcher.close();
|
|
} catch (error) {
|
|
logger.warn(`[fusion:dev] source watcher close failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|