Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
154 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
ecd989f
Merge branch 'job/integration' into job/p2-health
m4ttheweric Aug 28, 2026
17cd7fd
Merge branch 'job/integration' into job/p6-portability
m4ttheweric Aug 28, 2026
1792720
spec: p2-health daemon health model design
m4ttheweric Aug 28, 2026
e130062
spec: p6-portability design (Phase 6 portability)
m4ttheweric Aug 28, 2026
4ed21e0
spec: fix degraded-vs-alive-not-serving branch, pin thresholds, scope…
m4ttheweric Aug 28, 2026
90ce759
spec: apply reviewer edits (S069 read contract + 3 sites, bounded-pre…
m4ttheweric Aug 28, 2026
52f450e
plan: p2-health implementation plan (16 TDD tasks)
m4ttheweric Aug 28, 2026
803c960
plan: p6-portability implementation plan (10 tasks)
m4ttheweric Aug 28, 2026
dcca3ff
plan: apply reviewer fixes (loop-monitor test/alloc, CacheRefresherDe…
m4ttheweric Aug 28, 2026
3dcbfe7
add lib/daemon/health.ts: pure computeHealth + thresholds
m4ttheweric Aug 28, 2026
b8f1572
add lib/daemon/heartbeat-file.ts: atomic-rename heartbeat
m4ttheweric Aug 28, 2026
e9b22f2
plan: apply reviewer fixes (bounded read in links.ts, widen disabledR…
m4ttheweric Aug 28, 2026
25c73c5
add lib/daemon/loop-monitor.ts: drift monitor + heartbeat cadence
m4ttheweric Aug 28, 2026
cc5374b
rt-client: register rt.daemonPath machine setting (6.1)
m4ttheweric Aug 28, 2026
d2b3e21
daemon-status: heartbeat-stale 'stalled' detail + degraded eventLoop
m4ttheweric Aug 28, 2026
38ffaa8
daemon status: render health/stall lines; add non-restarting pingDaemon
m4ttheweric Aug 28, 2026
319849a
user-path: async fish-aware killable PATH probe + rt.daemonPath overr…
m4ttheweric Aug 28, 2026
258973b
daemon-logger: stream error listener + loggerDegraded + crash-handler…
m4ttheweric Aug 28, 2026
4b2e199
daemon-logger: drop em dashes from new comments (style)
m4ttheweric Aug 28, 2026
b8a9948
user-path: reword overlay comment to drop process-artifact reference
m4ttheweric Aug 28, 2026
36ebab3
daemon: await async resolveUserPath; drop user-path sync-exec allowli…
m4ttheweric Aug 28, 2026
494bb52
daemon-logger: rt.logLevel resolution, stderr->warn demotion, 50m siz…
m4ttheweric Aug 28, 2026
65ca166
home: stable machine-key at init, data-preserving freeze of existing …
m4ttheweric Aug 28, 2026
40aeb13
log-janitor: onError callback; daemon logs prune failures at warn
m4ttheweric Aug 28, 2026
7e83441
plan: Task 14 imports setSettingsWarnSink from local ./settings/resol…
m4ttheweric Aug 28, 2026
4c725d1
rt-client: rt.logLevel registry row + injectable deduped settings war…
m4ttheweric Aug 28, 2026
3b7daf6
dev-mode: marker-based wrapper detection, bounded read, legacy fallba…
m4ttheweric Aug 28, 2026
384b8a1
handleCommand: reqId + caller tag + per-(cmd,error) suppression + slo…
m4ttheweric Aug 28, 2026
8ca4d9b
setup: arm64/unsupported-arch row at setup (R051)
m4ttheweric Aug 28, 2026
e877114
unknown-command envelope (code+version); transports send X-RT-Client …
m4ttheweric Aug 28, 2026
e75d566
servers: thread X-RT-Client into payload._client; advertise it in CORS
m4ttheweric Aug 28, 2026
46ac573
plan: split HandlerContext type additions into the tasks that provide…
m4ttheweric Aug 28, 2026
a410818
ctx: extend refreshStatusRef with cycle outcome; export apiWsClientCount
m4ttheweric Aug 28, 2026
af36aec
cache-refresh: remove em dash from applyRefreshOutcome docstring
m4ttheweric Aug 28, 2026
dd3353b
dev-mode: fix rt-health/steps-a consumer tests for the new wrapper de…
m4ttheweric Aug 28, 2026
8f3b92a
home-snapshot: diagnose not-provisioned and missing git identity (S09…
m4ttheweric Aug 28, 2026
0400416
daemon: wire loop monitor + heartbeat + health sampler; surface healt…
m4ttheweric Aug 28, 2026
38cf561
add rt daemon log-level: live level set/show via daemon:log-level verb
m4ttheweric Aug 28, 2026
22a2f52
fix: remove em dashes from log-level comments
m4ttheweric Aug 28, 2026
16de2ba
home-snapshot: gate janitor-zone commit on git identity; drop em dash…
m4ttheweric Aug 28, 2026
2753782
secrets: timeout + SecretsTimeoutError on the sops spawn (S070 sops h…
m4ttheweric Aug 28, 2026
81928fd
e2e: assert additive health/metrics/eventLoop + heartbeat file
m4ttheweric Aug 28, 2026
16d071d
branch-cache: add composeKey/branchOf/identityOf + get/getByBranch (S…
m4ttheweric Aug 28, 2026
1186b79
registry.test: add rt.logLevel to migrated-key fixtures (24 keys)
m4ttheweric Aug 28, 2026
23a6d3d
branch-cache: make get/getByBranch free functions; restore daemon.ts …
m4ttheweric Aug 28, 2026
d8de94f
fix: windowed maxLag recovery + named loop-lag threshold + review fix…
m4ttheweric Aug 28, 2026
3ac78f2
fix: drop em dashes in arch-row comment and test describe (no-em-dashes)
m4ttheweric Aug 29, 2026
e966d03
branch-cache: flip to composite ${identity}:${branch} key; scope all …
m4ttheweric Aug 29, 2026
12f8967
test: adapt dev-mode + home-init consumer fixtures to stricter detect…
m4ttheweric Aug 29, 2026
1e79eb3
docs: regenerate command reference for wave-1 daemon flags and verbs
m4ttheweric Aug 29, 2026
730d008
enrich: repo-scope daemon cache:read; SecretsTimeoutError.name (final…
m4ttheweric Aug 29, 2026
b7ee957
merge job/integration (wave 1) into job/integration-2
m4ttheweric Aug 29, 2026
553fd6a
merge job/p2-health (phase 2: daemon health) into job/integration-2
m4ttheweric Aug 29, 2026
00bb2b1
merge job/p6-portability (phase 6: cross-machine portability) into jo…
m4ttheweric Aug 29, 2026
e8d8417
ci: retrigger checks after docs regeneration
m4ttheweric Aug 29, 2026
afe0f90
docs: regenerate command reference after wave-2 merge (J5)
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
34d4a10
merge job/integration (main absorbed) into job/integration-2
m4ttheweric Aug 29, 2026
bb73645
merge main (wave 1 squashed) into job/integration-2
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
10 changes: 10 additions & 0 deletions commands/__tests__/home.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,6 +172,16 @@ class FakeSeam implements ExecSeam {
async run(cmd: string[]): Promise<ExecResult> {
this.calls.push({ kind: "run", arg: cmd });
if (this.opts.failRun?.(cmd)) return { code: 1, stdout: "", stderr: "boom" };
// commitInitialUserRepo probes identity before committing; default to
// "configured" so fixtures not exercising the no-identity path stay
// green, mirroring lib/daemon/__tests__/home-snapshot.test.ts's
// defaultResponders.
if (cmd[cmd.length - 2] === "config" && cmd[cmd.length - 1] === "user.name") {
return { code: 0, stdout: "rt test\n", stderr: "" };
}
if (cmd[cmd.length - 2] === "config" && cmd[cmd.length - 1] === "user.email") {
return { code: 0, stdout: "rt@example.test\n", stderr: "" };
}
return { code: 0, stdout: "", stderr: "" };
}
async writeFile(path: string, content: string): Promise<void> {
Expand Down
9 changes: 9 additions & 0 deletions commands/__tests__/log-level.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
import { test, expect } from "bun:test";
import { formatLogLevelResult } from "../daemon.ts";

test("formats a set result", () => {
expect(formatLogLevelResult({ ok: true, level: "debug" }, true)).toContain("debug");
});
test("formats a show result", () => {
expect(formatLogLevelResult({ ok: true, level: "info" }, false)).toContain("info");
});
33 changes: 33 additions & 0 deletions commands/__tests__/status-lines.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
import { test, expect } from "bun:test";
import { statusLines } from "../daemon.ts";

const strip = (s: string) => s.replace(/\[[0-9;]*m/g, "");

test("degraded/unresponsive prints ping-carried maxLag, not 'likely mid-sync'", () => {
const lines = statusLines(
{ state: "degraded", reason: "unresponsive", pid: 42, eventLoop: { maxLagMs: 1400, lastStallAt: 1, lastStallCmd: "mr:action", stalls: 2 } } as any,
2000,
).map(strip).join("\n");
expect(lines).not.toContain("likely mid-sync");
expect(lines).toContain("1400ms");
expect(lines).toContain("mr:action");
});

test("alive-not-serving 'stalled' prints stalled Ns ago", () => {
const lines = statusLines(
{ state: "alive-not-serving", pid: 42, detail: "stalled", stalledForMs: 8000 } as any,
0,
).map(strip).join("\n");
expect(lines).toContain("event loop stalled");
expect(lines).toContain("8s");
});

test("running prints the health level and reasons when present", () => {
const lines = statusLines(
{ state: "running", data: { pid: 42, uptime: 60000, watchedRepos: 3, cacheEntries: 10,
health: { level: "degraded", reasons: ["refresh: 3 repos failing (auth?)"] } } } as any,
0,
).map(strip).join("\n");
expect(lines).toContain("degraded");
expect(lines).toContain("refresh: 3 repos failing");
});
55 changes: 46 additions & 9 deletions commands/daemon.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,11 +33,12 @@ import {
LOG_DIR,
LAUNCHD_PLIST_PATH,
} from "../lib/daemon-config.ts";
import { daemonQuery, isDaemonRunning, trayQuery } from "../lib/daemon-client.ts";
import { daemonQuery, isDaemonRunning, pingDaemon, trayQuery } from "../lib/daemon-client.ts";
import { classifyDaemonStatus, type DaemonStatusVerdict } from "../lib/daemon-status.ts";
import { resolveIntendedMode, currentMode, type IntendedMode } from "../lib/dev-mode.ts";
import { probeSocketHolder } from "../lib/daemon/park.ts";
import { readBreadcrumb, readSupervisionState } from "../lib/daemon/supervision-state.ts";
import { readHeartbeat } from "../lib/daemon/heartbeat-file.ts";
import { runCapture } from "../lib/subprocess.ts";
import { isGitLabRemote } from "../lib/enrich.ts";
import type { CacheKind, RepoTrackingEntry } from "../lib/repo-tracking.ts";
Expand DownExpand Up@@ -403,22 +404,27 @@ export async function showStatus(args: string[] = []): Promise<void> {
// A failed status query does NOT mean the daemon is down — it answers `ping`
// in a fraction of the budget a loaded `status` needs. Establish liveness
// before reporting, and only pay for the probe when nothing came back.
const pingOk = classifyDaemonStatus.needsLivenessProbe(response) ? await isDaemonRunning() : false;
// pingDaemon (not isDaemonRunning) so the raw reply's eventLoop summary is
// still on hand to render, and so this probe never risks a restart.
const pingResp = classifyDaemonStatus.needsLivenessProbe(response) ? await pingDaemon() : null;
const pingOk = pingResp?.ok === true;
const recordedPid = readDaemonPid() ?? null;

// Ping ALSO failed: the only remaining ground is the pid/breadcrumb/kv
// trail Task 9 left behind. Read it here, once, rather than on every status
// Ping ALSO failed: the only remaining ground is the pid/breadcrumb/kv/heartbeat
// trail Task 9/2 left behind. Read it here, once, rather than on every status
// call, since it's the uncommon path.
let pidAlive: boolean | undefined;
let pid = recordedPid;
let breadcrumb: ReturnType<typeof readBreadcrumb> | undefined;
let supervision: ReturnType<typeof readSupervisionState> | undefined;
let heartbeat: ReturnType<typeof readHeartbeat> | undefined;
if (classifyDaemonStatus.needsPidProbe(response, pingOk)) {
breadcrumb = readBreadcrumb();
// The kv tier can be legitimately empty (or reflect nothing useful) when
// a failure happened before state.db ever opened (Ruling P1). The
// breadcrumb read above is what classifyDaemonStatus falls back to then.
supervision = readSupervisionState();
heartbeat = readHeartbeat(RT_DIR);
const probed = await probePidAlive(recordedPid, breadcrumb?.pid);
pidAlive = probed.alive;
pid = probed.pid;
Expand All@@ -433,6 +439,8 @@ export async function showStatus(args: string[] = []): Promise<void> {
intendedFlavor: resolveIntendedMode().mode,
breadcrumb,
supervision,
heartbeat,
pingEventLoop: (pingResp as any)?.eventLoop,
});

if (json) return void console.log(JSON.stringify({ ok: true, ...verdict }));
Expand DownExpand Up@@ -503,6 +511,15 @@ export function statusLines(verdict: DaemonStatusVerdict, now: number): string[]
});
lines.push(` ${dim}events: ${parts.join(" · ")}${reset}`);
}

const health = verdict.data.health as { level: string; reasons: string[] } | undefined;
if (health && health.level !== "ok") {
const dot = health.level === "unhealthy" ? red : yellow;
lines.push(` ${dot}health: ${health.level}${reset}`);
for (const r of health.reasons) lines.push(` ${dim}- ${r}${reset}`);
}
const el = verdict.data.eventLoop as { maxLagMs: number } | undefined;
if (el && el.maxLagMs >= 500) lines.push(` ${dim}event loop: maxLag ${el.maxLagMs}ms${reset}`);
return lines;
}

Expand All@@ -511,11 +528,14 @@ export function statusLines(verdict: DaemonStatusVerdict, now: number): string[]
// the operator to `rt daemon start` against a daemon that is already up.
const lines = [` ${yellow}●${reset} running, but not reporting status`];
if (verdict.pid) lines.push(` ${dim}pid: ${verdict.pid}${reset}`);
lines.push(
verdict.reason === "error"
? ` ${dim}status command failed: ${verdict.detail ?? "unknown error"}${reset}`
: ` ${dim}answers ping, but status timed out — likely mid-sync${reset}`,
);
if (verdict.reason === "error") {
lines.push(` ${dim}status command failed: ${verdict.detail ?? "unknown error"}${reset}`);
} else if (verdict.eventLoop && verdict.eventLoop.maxLagMs > 0) {
const el = verdict.eventLoop;
lines.push(` ${dim}answers ping, status timed out: event loop maxLag ${el.maxLagMs}ms${el.lastStallCmd ? ` (last stall in ${el.lastStallCmd})` : ""}${reset}`);
} else {
lines.push(` ${dim}answers ping, but status timed out — likely mid-sync${reset}`);
}
lines.push(` ${dim}check: rt daemon logs${reset}`);
return lines;
}
Expand All@@ -536,6 +556,7 @@ export function statusLines(verdict: DaemonStatusVerdict, now: number): string[]
booting: "still booting",
wedged: "reached ready but stopped answering (likely deadlocked)",
quarantined: "recovered from a corrupt db but still not answering",
stalled: `event loop stalled ${Math.round((verdict.stalledForMs ?? 0) / 1000)}s ago (no heartbeat)`,
}[verdict.detail];
return [
` ${yellow}●${reset} process ${verdict.pid} is running but not answering rt.sock`,
Expand DownExpand Up@@ -1145,3 +1166,19 @@ async function waitForPort(port: number, timeoutMs: number): Promise<void> {
await new Promise(r => setTimeout(r, 100));
}
}

/** Pure formatter for `daemon:log-level` results, shared by the CLI and its tests. */
export function formatLogLevelResult(res: { ok: boolean; level?: string; error?: string }, wasSet: boolean): string {
if (!res.ok) return ` ${red}●${reset} ${res.error ?? "failed"}`;
return ` ${green}●${reset} daemon log level ${wasSet ? "set to" : "is"} ${res.level}`;
}

/** Show (no arg) or set (level arg) the running daemon's live pino log level. */
export async function setLogLevel(args: string[] = []): Promise<void> {
const json = args.includes("--json");
const level = args.find((a) => !a.startsWith("--"));
const res = await daemonQuery("daemon:log-level", level ? { level } : {});
if (!res) { console.log(` ${red}●${reset} daemon not reachable`); return; }
if (json) { console.log(JSON.stringify(res)); return; }
console.log(formatLogLevelResult(res as any, Boolean(level)));
}
3 changes: 2 additions & 1 deletion commands/home.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,7 @@ import { join } from "path";
import type { CommandContext } from "../lib/command-tree.ts";
import { bold, dim, green, red, reset, yellow } from "../lib/ansi.ts";
import { isSafeMachineKeySegment, machineKey, mattstackHome } from "../lib/rt-paths.ts";
import { resolveInitialMachineKey } from "../lib/home/machine-id.ts";
import {
buildInitPlan,
chooseMachineProfile,
Expand DownExpand Up@@ -549,7 +550,7 @@ export async function homeInit(args: string[], _ctx: CommandContext = {}, seams:
const exec = seams.exec ?? createRealExecSeam(mattstackHome());
const ageKeySeam = seams.ageKeySeam ?? createRealAgeKeySeam();
const sopsYamlSeam = seams.sopsYamlSeam ?? defaultSopsYamlSeam();
const key = seams.key ?? machineKey();
const key = seams.key ?? (await resolveInitialMachineKey(mattstackHome(), probes));
const pickerSeam = seams.pickerSeam ?? createRealMachineProfilePickerSeam();
const isInteractive = seams.isInteractive ?? (() => Boolean(process.stdin.isTTY));
const materializeExec = seams.materializeExec ?? defaultMaterializeExec();
Expand Down
3 changes: 2 additions & 1 deletion commands/settings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
TRAY_APP_NAME, DEV_TRAY_APP_NAME, TRAY_APP_BUNDLE,
trayAppPath, devTrayAppPath,
} from "../lib/rt-paths.ts";
import { installRtBinary } from "../lib/dev-mode.ts";
import { DEV_MODE_TAG, installRtBinary } from "../lib/dev-mode.ts";
import { describeTuple, tupleWarning, type FlavorTuple } from "./daemon.ts";
import { RT_BUNDLE_PATH } from "../lib/bundle-layout.ts";
import { spawnSync } from "child_process";
Expand DownExpand Up@@ -511,6 +511,7 @@ export function renderDevModeWrapper(sourcePath: string, bunPath: string): strin
const bunDir = dirname(bunPath);
return [
`#!/bin/zsh`,
`${DEV_MODE_TAG}`,
`export PATH="${bunDir}:/opt/homebrew/bin:/usr/local/bin:$PATH"`,
`export RT_LAUNCH_CWD="$PWD"`,
`cd "${sourcePath}" || { echo "rt: dev-mode source checkout missing: ${sourcePath}" >&2; exit 1; }`,
Expand Down
24 changes: 24 additions & 0 deletions commands/status/__tests__/status-fallback.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,6 +87,30 @@ describe("rt status fallback (no daemon)", () => {
expect(existsSync(rtDirPath) ? readdirSync(rtDirPath) : []).toEqual([]);
});

test("S069/Task 10: two repos sharing a branch name display as a bare branch, not a composite key", async () => {
const dbPath = stateDbPath();
mkdirSync(join(home, ".mattstack", "rt"), { recursive: true });
const db = openStateDb(dbPath);
const store = getBranchCacheStore(db);
store.put("shared-branch", {
ticket: null, linearId: "", mr: null, fetchedAt: 1, repoName: "repo-a",
});
store.put("shared-branch", {
ticket: null, linearId: "", mr: null, fetchedAt: 2, repoName: "repo-b",
});
db.close();

const data = await fetchStatusData();

// Both rows are real, distinct composite-keyed rows in state.db...the
// dashboard's flat bare-branch dict can only show one, never a raw
// composite key, and never crashes reconciling the two.
expect(Object.keys(data.branches)).toEqual(["shared-branch"]);
const winner = data.branches["shared-branch"]!.repoName;
expect(winner).toBeDefined();
expect(["repo-a", "repo-b"]).toContain(winner!);
});

test("an empty branch_cache table serves an empty dashboard", async () => {
mkdirSync(join(home, ".mattstack", "rt"), { recursive: true });
openStateDb(stateDbPath()).close();
Expand Down
3 changes: 2 additions & 1 deletion commands/status/data.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@

import type { CacheEntry, StatusData } from "./types.ts";
import type { PortEntry } from "../../lib/port-scanner.ts";
import { branchOf } from "../../lib/state/branch-cache.ts";

interface BranchCacheRow {
branch: string;
Expand DownExpand Up@@ -56,7 +57,7 @@ async function readBranchesFromStateDb(): Promise<Record<string, CacheEntry>> {
// with looser optionality on the ticket fields (`stateName?: string`
// vs `string | null`). Same data that used to arrive here as parsed
// JSON out of branch-cache.json.
branches[row.branch] = {
branches[branchOf(row.branch)] = {
ticket: row.ticket !== null ? (JSON.parse(row.ticket) as CacheEntry["ticket"]) : null,
linearId: row.linear_id,
mr: row.mr !== null ? (JSON.parse(row.mr) as CacheEntry["mr"]) : null,
Expand Down
Loading
Loading