Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
bafe1b3
agent-herdr: resolve herdr via PATH probe instead of hardcoded ~/.loc…
m4ttheweric Aug 28, 2026
fa5f64b
notifier: retry the post-push queue removal so a busy-swallowed delet…
m4ttheweric Aug 28, 2026
ce9c8d6
cron: pass the daemon's resolved process.env to spawn instead of the …
m4ttheweric Aug 28, 2026
52be3d9
agent handler: return ok:false when herdr dedups the tab label instea…
m4ttheweric Aug 28, 2026
94d6218
chat handlers: guard emit/notify after the message commit so a throw …
m4ttheweric Aug 28, 2026
7b061da
discussions:diffs: bound the GitLab fetch with a timeout+abort signal…
m4ttheweric Aug 28, 2026
cc924b5
plan: Phase 0 honest-supervision implementation plan
m4ttheweric Aug 28, 2026
71ca8a9
hooks-guard: handle fs.watch error events and reconcile stale watcher…
m4ttheweric Aug 28, 2026
b944a2d
plan: Phase 3 trust-boundary implementation plan (S005/S006/S010/S040…
m4ttheweric Aug 28, 2026
2246ec9
deps links: never auto-unlink a DEFAULT_EXPOSED tool (rt/fast-browser…
m4ttheweric Aug 28, 2026
def840f
age-key: give the keychain spawn a bounded, distinguishable timeout (…
m4ttheweric Aug 28, 2026
c0c1ff0
presence-store: honor the tail heartbeat in the offline rule and prun…
m4ttheweric Aug 28, 2026
fd626d8
chat:sign-in: reject a missing/empty sessionId instead of storing a N…
m4ttheweric Aug 28, 2026
5651d9a
worktree trash: exclude .worktrees/ from the repo's git status on eve…
m4ttheweric Aug 28, 2026
b64dac7
docs: sketch daemon supervision verdicts + exit-code semantics
m4ttheweric Aug 28, 2026
f0ffdbb
docs: retire stale daemon-runner-health.md, point at the current audi…
m4ttheweric Aug 28, 2026
a15ed3e
daemon: cap request body size at 1 MiB on both servers (S092)
m4ttheweric Aug 28, 2026
cd20722
worktree trash reap: require a plausible rt-written epoch, and refuse…
m4ttheweric Aug 28, 2026
32fd197
daemon: one shared api-token cache for api-server and secrets handler…
m4ttheweric Aug 28, 2026
421d67b
daemon: boot failure is fatal (exit 1), gated by boot-phase flag; rt.…
m4ttheweric Aug 28, 2026
8be75f0
pane:spawn: check the caller's abort signal between steps and stop ea…
m4ttheweric Aug 28, 2026
eea6451
mr:by-branch: apply the same demand-scope gate to the forge write-bac…
m4ttheweric Aug 28, 2026
3bd511b
plan: Phase 1 event-loop implementation plan (RT-78, items 1.1-1.5)
m4ttheweric Aug 28, 2026
b7694dd
daemon: drop startDaemon's dead-code catch and its now-stale JSDoc
m4ttheweric Aug 28, 2026
b24bb2b
worktree create: serialize createTree per repoPath so provision and r…
m4ttheweric Aug 28, 2026
8da0e04
daemon: rt.trustedBrowserOrigins allowlist + needsToken invert-defaul…
m4ttheweric Aug 28, 2026
40e1e35
herdr client: accumulate raw socket bytes and decode once instead of …
m4ttheweric Aug 28, 2026
d34dfbf
daemon: install stderr redirect + crash handlers before every module-…
m4ttheweric Aug 28, 2026
43c5486
port-scanner: canonicalize repo/worktree paths once per scan so a sym…
m4ttheweric Aug 28, 2026
ff751ee
rt-paths: only migrate a legacy ~/.rt that carries an actual rt signa…
m4ttheweric Aug 28, 2026
8e689b0
daemon: fix needsToken to never gate OPTIONS preflight (review fix)
m4ttheweric Aug 28, 2026
e8d0a7c
daemon-client: return timed-out/refused attribution per query instead…
m4ttheweric Aug 28, 2026
4448554
daemon: default-deny CORS and gate /ws on origin/token (S005/S006)
m4ttheweric Aug 28, 2026
6db63b8
runCapture: race reads against the deadline so a pipe-holding grandch…
m4ttheweric Aug 28, 2026
59fc77a
daemon-client: bounded-poll rt.sock after a restart instead of one 30…
m4ttheweric Aug 28, 2026
0d816c0
runs prune: reap expired run dirs with a detached rm -rf, never a syn…
m4ttheweric Aug 28, 2026
cbba6b4
daemon: broadcast() drops dead/backpressured ws clients instead of si…
m4ttheweric Aug 28, 2026
659ecee
worktree reconciler: queue a follow-up pass for a kick arriving after…
m4ttheweric Aug 28, 2026
ba5e7fc
events.db: corruption quarantine + busy_timeout/synchronous pragmas; …
m4ttheweric Aug 28, 2026
b86d4c2
runCapture: stop clearing the SIGKILL timer before it fires, strength…
m4ttheweric Aug 28, 2026
456b8b8
chat:post/chat:dm: validate room/handle/body shape and refuse an unkn…
m4ttheweric Aug 28, 2026
5657d27
daemon: pathParam() helper -- malformed %-encoding is a 400, not a lo…
m4ttheweric Aug 28, 2026
e508951
validate chat:join wakeOn and agent:start/resume surface against thei…
m4ttheweric Aug 28, 2026
4b9afad
source-guards: update the boot-failure-exits guard for runDaemon owni…
m4ttheweric Aug 28, 2026
fd935d7
git-async: 5-min timeout for checkout/merge/stash/status so a large-r…
m4ttheweric Aug 28, 2026
2291068
chat:read/chat:messages: clamp limit into [1,500] instead of reaching…
m4ttheweric Aug 28, 2026
0bdb7e8
daemon: coerce REST GET query params to number/boolean at the seam (S…
m4ttheweric Aug 28, 2026
d96051b
git-async: bump the 3 inline runGit(status/stash pop) call sites to M…
m4ttheweric Aug 28, 2026
746515d
discussions-diffs test: fix fetch cast to satisfy tsc
m4ttheweric Aug 28, 2026
2bd75c6
daemon: log the EADDRINUSE port holder and throw a typed ApiPortInUse…
m4ttheweric Aug 28, 2026
150486f
cache-refresh: async git + grant-gated doppler loop; add listWorktree…
m4ttheweric Aug 28, 2026
f292058
daemon: bind API before socket; register rt.apiPort setting + resolve…
m4ttheweric Aug 28, 2026
a458351
daemon: standalone git-ref validator for option-injection guard (work…
m4ttheweric Aug 28, 2026
977c03d
daemon: standalone credential-redaction utility (freshness.ts wiring …
m4ttheweric Aug 28, 2026
7d1a2fa
freshness: async, cached getRemoteUrl via runCapture (R032)
m4ttheweric Aug 28, 2026
cf52596
worktree-process-kill: async lsof/ps via runCapture (S015, S016)
m4ttheweric Aug 28, 2026
6d950e3
home-snapshot: lazy daemon-flavored state.db; getStateDb re-applies a…
m4ttheweric Aug 28, 2026
baaffc9
docs: the :9401 trust boundary model and the S010/S050/S043 sibling w…
m4ttheweric Aug 28, 2026
fed0ce3
repo-index: async observed-main-path on the endpoint:claim resolve pa…
m4ttheweric Aug 28, 2026
a7bf256
fix rt-paths lint and registry test regressions on job/p3-trust-boundary
m4ttheweric Aug 28, 2026
3037b27
state: isBusyError matches SQLITE_BUSY_*; read-then-write daemon txns…
m4ttheweric Aug 28, 2026
f76d994
repo-index: fix write-back regression in resolveIndexPathForIdentity
m4ttheweric Aug 28, 2026
63e5ec5
gate: fail on sync-exec anywhere in the daemon import graph (1.3)
m4ttheweric Aug 28, 2026
56e7f14
state.db: isolate each legacy importer in a SAVEPOINT so one bad file…
m4ttheweric Aug 28, 2026
9783b1c
daemon: add supervision-state (boot attempts, failures, last-exit) kv…
m4ttheweric Aug 28, 2026
673cfcf
refresh: whole-cycle deadline clears the coalesce latch; cap RepoWatc…
m4ttheweric Aug 28, 2026
fcf8220
daemon: wire supervision-state breadcrumbs + kv records into boot/shu…
m4ttheweric Aug 28, 2026
fa6523f
fix(refresh): clear the deadline timer on every settle path, not just…
m4ttheweric Aug 28, 2026
e9cdbd8
freshness: rebuild the provider cache when gitlabToken rotates (S048,…
m4ttheweric Aug 28, 2026
1af0f28
daemon: lazy-resolve trusted-origin allowlist, drop finding-ID comments
m4ttheweric Aug 28, 2026
fbf7ba8
docs: name the S084 GET-route known-gap ruling in the trust-boundary …
m4ttheweric Aug 28, 2026
88da406
freshness: close the two remaining stale-token paths (S048/S049 revie…
m4ttheweric Aug 28, 2026
58ab7e4
system-process-scanner: a failed lsof preserves runaway windows (S061)
m4ttheweric Aug 28, 2026
3c090d3
runs: mtime-memoize finished-run summaries; back off herdr probe (S10…
m4ttheweric Aug 28, 2026
e9313e0
daemon status: alive-not-serving / parked / boot-failed / crash-loopi…
m4ttheweric Aug 28, 2026
063dd29
runs: scope summary-cache key by runsRoot; poller: test the backoff r…
m4ttheweric Aug 28, 2026
78803f5
pollers: gate the 10s/30s scans on recent consumer demand (S058, S093)
m4ttheweric Aug 28, 2026
983020b
daemon status: exclude the calling process from probePidAlive's lsof …
m4ttheweric Aug 28, 2026
fce330f
demand-tracker: document command-only demand-stamp contract; subproce…
m4ttheweric Aug 28, 2026
f2dfbf0
logs: rotate daemon-stderr.log on open; hide stale crash block by mtime
m4ttheweric Aug 28, 2026
3366ff8
daemon: bare-signal exit is non-zero (launchd respawns); shutdown ver…
m4ttheweric Aug 28, 2026
33a8359
daemon: ownership-aware socket/pid unlink; eviction waits for pid dea…
m4ttheweric Aug 28, 2026
e84f667
daemon CLI: uninstall guards on liveness; start escalates to kickstar…
m4ttheweric Aug 28, 2026
919100e
daemon: redirect stderr after legacy-dir migration; set shuttingDownV…
m4ttheweric Aug 28, 2026
4e0af1e
daemon: drop unused clearBreadcrumb
m4ttheweric Aug 28, 2026
2fa055c
rt-client: soften the rt.apiPort description to match reality
m4ttheweric Aug 28, 2026
0badca0
docs: scrub em/en dashes, fix daemon-supervision-design phase-order d…
m4ttheweric Aug 28, 2026
05a8f18
chore: scrub em/en dashes from branch-added comments and strings (own…
m4ttheweric Aug 28, 2026
93a98a4
Merge branch 'job/bounded-batch' into job/integration
m4ttheweric Aug 28, 2026
2e03f36
Merge branch 'job/p3-trust-boundary' into job/integration
m4ttheweric Aug 28, 2026
5b77738
Merge branch 'job/p1-event-loop' into job/integration
m4ttheweric Aug 28, 2026
026b4cb
Merge branch 'job/p0-supervision' into job/integration
m4ttheweric Aug 28, 2026
2852e9d
I5: wire the seams the four lanes documented but could not cross
m4ttheweric Aug 28, 2026
f395575
e2e: rewrite the 3 API-port-squat daemon tests for I5(a)'s park-retry
m4ttheweric Aug 28, 2026
1e79eb3
docs: regenerate command reference for wave-1 daemon flags and verbs
m4ttheweric Aug 29, 2026
e8d8417
ci: retrigger checks after docs regeneration
m4ttheweric Aug 29, 2026
92f165b
merge main into job/integration
m4ttheweric Aug 29, 2026
68c2618
chat-handlers.test: drop test for chat:unread-waking, a verb main's d…
m4ttheweric Aug 29, 2026
161d588
api-auth: allow token-authenticated browser preflight for off-allowli…
m4ttheweric Aug 29, 2026
ab21119
boot-reconcile: treat SIGTERM ESRCH race as benign eviction, not boot…
m4ttheweric Aug 29, 2026
2c47429
cache-refresh: bound stalled-cycle accumulation with an orphan cap
m4ttheweric Aug 29, 2026
f44f0b4
freshness: invalidate cached provider when gitlabToken is removed, no…
m4ttheweric Aug 29, 2026
855e872
chat handlers: thread ctx.log through instead of a module-private laz…
m4ttheweric Aug 29, 2026
aa82c3f
age-key: settle the seam.run deadline independently of proc.exited, e…
m4ttheweric Aug 29, 2026
8693c26
api-server-bind.test: restore RT_API_PORT and unset rt.apiPort in aft…
m4ttheweric Aug 29, 2026
c4d43b2
api-server: build advertised docs/websocket URLs from the resolved bi…
m4ttheweric Aug 29, 2026
65017f2
agent-herdr: merge the supplied env into the herdr child, not bare pr…
m4ttheweric Aug 29, 2026
2511977
daemon: set state-db phase before opening the store; log boot flush f…
m4ttheweric Aug 29, 2026
b3a14f4
presence-store: fix prune SQL leg leak on signed-out rows; use immedi…
m4ttheweric Aug 29, 2026
5af1f1c
api-server-cors-ws.test: fix tsc unknown-type errors on fetch().json(…
m4ttheweric Aug 29, 2026
a6a67cc
merge main into job/integration
m4ttheweric Aug 29, 2026
6f46b19
docs: regenerate reference for pane:focus
m4ttheweric Aug 29, 2026
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
38 changes: 38 additions & 0 deletions commands/__tests__/daemon-logs-render.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
/**
* nativeStderrDisplay (showLogs' stale-crash mtime gate).
*
* daemon-stderr.log is rotated on daemon boot (lib/daemon-logger.ts), but a
* leftover file can still predate the *currently running* daemon (e.g. it was
* never rotated because the daemon has been up for days). This pins the
* show/hide + header decision without needing a live daemon or a real file.
*/

import { describe, test, expect } from "bun:test";
import { nativeStderrDisplay } from "../daemon.ts";

const NOW = 1_785_000_000_000;

describe("nativeStderrDisplay", () => {
test("hides the block when the file predates the daemon's startedAt", () => {
const { show, header } = nativeStderrDisplay(NOW - 10_000, NOW);
expect(show).toBe(false);
expect(header).toBe("no crash since this daemon started");
});

test("hides the block when the file mtime exactly equals startedAt", () => {
const { show } = nativeStderrDisplay(NOW, NOW);
expect(show).toBe(false);
});

test("shows the block, with the mtime in the header, when the file postdates startedAt", () => {
const mtimeMs = NOW + 5_000;
const { show, header } = nativeStderrDisplay(mtimeMs, NOW);
expect(show).toBe(true);
expect(header).toBe(`native stderr (captured ${new Date(mtimeMs).toISOString()})`);
});

test("fails open (shows) when the daemon's startedAt is unknown (nothing to compare against)", () => {
const { show } = nativeStderrDisplay(NOW - 999_999, null);
expect(show).toBe(true);
});
});
37 changes: 37 additions & 0 deletions commands/__tests__/daemon-status-render.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,4 +71,41 @@ describe("statusLines", () => {
expect(out).toContain("watching: 1 repo");
expect(out).not.toContain("1 repos");
});

// ── Task 10 ──

test("a parked pid points at the flavor mismatch, not 'not running'", () => {
const out = plain({ state: "parked", pid: 42, holderFlavor: "prod" });
expect(out).toContain("parked");
expect(out).toContain("pid 42");
expect(out).toContain("held by: prod");
expect(out).not.toContain("installed but not running");
});

test("alive-not-serving names the pid and the stuck detail", () => {
const out = plain({ state: "alive-not-serving", pid: 42, detail: "booting" });
expect(out).toContain("process 42 is running but not answering rt.sock");
expect(out).toContain("still booting");
expect(out).not.toContain("installed but not running");
});

test("alive-not-serving wedged/quarantined get their own detail lines", () => {
expect(plain({ state: "alive-not-serving", pid: 1, detail: "wedged" })).toContain("deadlocked");
expect(plain({ state: "alive-not-serving", pid: 1, detail: "quarantined" })).toContain("recovered from a corrupt db");
});

test("crash-looping surfaces the failure count and the last reason", () => {
const out = plain({ state: "crash-looping", failures: 4, reason: "EADDRINUSE" });
expect(out).toContain("crash-looping");
expect(out).toContain("4 failures");
expect(out).toContain("EADDRINUSE");
});

test("boot-failed surfaces the phase and reason, and points at rt daemon start", () => {
const out = plain({ state: "boot-failed", reason: "EADDRINUSE", phase: "api" });
expect(out).toContain("boot failed");
expect(out).toContain("phase: api");
expect(out).toContain("EADDRINUSE");
expect(out).toContain("rt daemon start");
});
});
145 changes: 145 additions & 0 deletions commands/__tests__/daemon-uninstall-start.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
/**
* `rt daemon uninstall`/`start` (the CLI-side liveness guards, Task 14,
* S027/S030/S028-CLI). Fakes the tray over a real Bun.serve on
* TRAY_SOCK_PATH (same rig as commands/__tests__/settings-dev-mode.test.ts)
* and, where a scenario needs "the daemon is live", a real Bun.serve on
* DAEMON_SOCK_PATH answering /ping (isDaemonProcessRunning's pid check and
* probeSocketHolder/isDaemonRunning's socket ping are both exercised for
* real, never mocked module internals).
*/
import { afterEach, describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { start, uninstall } from "../daemon.ts";
import {
DAEMON_CONFIG_PATH,
DAEMON_PID_PATH,
DAEMON_SOCK_PATH,
RT_DIR,
TRAY_SOCK_PATH,
markDaemonInstalled,
} from "../../lib/daemon-config.ts";
import { resolveIntendedMode } from "../../lib/dev-mode.ts";

let servers: ReturnType<typeof Bun.serve>[] = [];
let logs: string[] = [];
const realLog = console.log;

function captureLogs(): void {
logs = [];
console.log = (...args: unknown[]) => { logs.push(args.map(String).join(" ")); };
}

function serveTray(handlers: Record<string, () => Response>): void {
servers.push(Bun.serve({
unix: TRAY_SOCK_PATH,
fetch(req) {
const url = new URL(req.url);
const handler = handlers[url.pathname];
return handler ? handler() : new Response("not found", { status: 404 });
},
}));
}

/** A real listener on rt.sock that answers /ping (what both isDaemonRunning()
* (daemon-client.ts) and probeSocketHolder() (lib/daemon/park.ts) fetch.
* Flavor defaults to the CURRENT intended mode (not a hardcoded "prod") so
* start()'s post-liveness warnIfWrongFlavor() check never fires a spurious
* mismatch when this file runs after another test flips mattstack.mode in
* the shared isolated HOME `bun test` uses for the whole process). */
function serveDaemonPing(body?: Record<string, unknown>): void {
const resolvedBody = body ?? { ok: true, pid: 4242, flavor: resolveIntendedMode().mode };
servers.push(Bun.serve({
unix: DAEMON_SOCK_PATH,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/ping") return Response.json(resolvedBody);
return new Response("not found", { status: 404 });
},
}));
}

afterEach(() => {
console.log = realLog;
for (const s of servers) { try { s.stop(true); } catch { /* already stopped */ } }
servers = [];
for (const p of [DAEMON_SOCK_PATH, DAEMON_PID_PATH, TRAY_SOCK_PATH, DAEMON_CONFIG_PATH]) {
try { rmSync(p); } catch { /* absent */ }
}
});

describe("uninstall (liveness guard)", () => {
test("leaves rt.pid/daemon.json when isDaemonProcessRunning() says the daemon is alive", async () => {
mkdirSync(RT_DIR, { recursive: true });
markDaemonInstalled();
writeFileSync(DAEMON_PID_PATH, String(process.pid)); // this test process is genuinely alive
// tray unreachable: trayQuery('/daemon/stop') resolves null (no server on TRAY_SOCK_PATH)

captureLogs();
await uninstall();

expect(existsSync(DAEMON_PID_PATH)).toBe(true); // cleanupDaemonFiles did NOT run
expect(JSON.parse(readFileSync(DAEMON_CONFIG_PATH, "utf8")).installed).toBe(true); // markDaemonUninstalled did NOT run
expect(logs.join("\n")).toContain("launchctl bootout");
});

test("leaves rt.sock/daemon.json when probeSocketHolder() finds a live holder (no rt.pid at all)", async () => {
mkdirSync(RT_DIR, { recursive: true });
markDaemonInstalled();
serveDaemonPing();

captureLogs();
await uninstall();

expect(JSON.parse(readFileSync(DAEMON_CONFIG_PATH, "utf8")).installed).toBe(true);
expect(logs.join("\n")).toContain("launchctl bootout");
});

test("cleans up rt.sock/rt.pid/daemon.json when nothing is alive", async () => {
mkdirSync(RT_DIR, { recursive: true });
markDaemonInstalled();
writeFileSync(DAEMON_PID_PATH, "999999"); // no such pid
writeFileSync(DAEMON_SOCK_PATH, ""); // stale file, not a real listener (probeSocketHolder's fetch fails)

captureLogs();
await uninstall();

expect(existsSync(DAEMON_PID_PATH)).toBe(false);
expect(JSON.parse(readFileSync(DAEMON_CONFIG_PATH, "utf8")).installed).toBe(false);
expect(logs.join("\n")).not.toContain("launchctl bootout");
expect(logs.join("\n")).toContain("daemon fully uninstalled");
});
});

describe("start (kickstart escalation)", () => {
test("falls back to /daemon/restart when the tray acks /daemon/start but the socket never comes up", async () => {
mkdirSync(RT_DIR, { recursive: true });
markDaemonInstalled();
let restartCalled = false;
serveTray({
"/daemon/start": () => Response.json({ ok: true }),
"/daemon/restart": () => { restartCalled = true; return Response.json({ ok: true }); },
});

captureLogs();
await start();

expect(restartCalled).toBe(true);
}, 20_000);

test("escalation succeeds once /daemon/restart actually brings the socket up", async () => {
mkdirSync(RT_DIR, { recursive: true });
markDaemonInstalled();
serveTray({
"/daemon/start": () => Response.json({ ok: true }),
"/daemon/restart": () => {
serveDaemonPing();
return Response.json({ ok: true });
},
});

captureLogs();
await start();

expect(logs.join("\n")).toContain("daemon started");
}, 20_000);
});
19 changes: 19 additions & 0 deletions commands/__tests__/probe-pid-alive.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
import { describe, test, expect } from "bun:test";
import { probePidAlive } from "../daemon.ts";
import { readSupervisionState } from "../../lib/daemon/supervision-state.ts";

describe("probePidAlive", () => {
// Regression: the lsof fallback must exclude the CALLING process itself.
// showStatus opens a bun:sqlite handle on state.db (inside RT_DIR) via
// readSupervisionState() immediately before this probe runs ... `lsof +D
// RT_DIR` then legitimately reports the calling CLI process as a live
// holder of the directory, with no daemon involved at all. Without the
// process.pid filter this self-matches and a genuinely dead daemon
// (no recorded pid, no breadcrumb pid) misclassifies as alive.
test("a state.db handle held by THIS process does not self-match as a live daemon", async () => {
readSupervisionState(); // opens (and keeps open) the isolated HOME's state.db
const result = await probePidAlive(null, undefined);
expect(result.alive).toBe(false);
expect(result.pid).toBeNull();
});
});
Loading
Loading