Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let show skill find-worktrees --json # progressive body load
let open ./agent.3md --json
let context --json
let doctor --json
let web # local, read-only Fleet dashboard
```

**Federation over relocation:** indexes host assets *in place* — never migrates
Expand Down Expand Up @@ -64,6 +65,19 @@ Any project that already uses fledge can install `let` the same way as
`fledge-plugin-memory` / `fledge-plugin-github`. Agents then call `fledge let …`
without a separate PATH setup.

### Local Fleet view

```bash
let web
# or: fledge let web
```

Starts a local-only dashboard at `http://127.0.0.1:8731`. Fleet is a Let-native
control room built from the federated index: it shows each local agent's status,
task or latest prompt, activity, timestamps, and project context. Expanded
cards may show bounded, redacted local session text from an already-indexed
session file. It never provides agent controls or shell execution.

### Standalone (any host: Claude, Codex, Cursor, Kimi, …)

```bash
Expand Down
16 changes: 16 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ for every kind.

All commands accept `--json` (default machine path for agents).

### `let web`

Start the local, read-only Fleet dashboard.

```bash
let web
let web --port 8732
fledge let web
```

The dashboard binds only to `127.0.0.1` (default port `8731`) and refreshes
from Let's structured index cards. It puts agents first, with projects as a
secondary view. Expand an agent to inspect its redacted task context, command,
latest prompt or update, timestamps, and recent session output when locally
available. It never provides controls, shell execution, or unredacted secrets.

### `let history` (alias: `usage`)

Rank hosts and projects by session activity (path-only, no transcripts).
Expand Down
1 change: 1 addition & 0 deletions specs/catalog/catalog.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Context packs never include session paths or bodies.
| `phraseHits` | Trigger phrase match (all words required). |
| `findGeminiInstructions` | GEMINI.md + ~/.gemini global. |
| `findGeminiSessions` | Path-only history/projects. |
| `antigravitySessionCards` | Discover local Antigravity CLI transcript metadata without inventing a project binding. |
| `findGeminiMemory` | Antigravity brain/knowledge path-only. |
| `findKimiSessions` | Path-only workspaces/sessions via workspaces.json. |
| `findKimiMemory` | Path-only user-history. |
Expand Down
108 changes: 108 additions & 0 deletions specs/web/web.spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
module: web
version: 1
status: active
files:
- src/web.ts
- src/fleet-adapters.ts
- src/fleet-brand.ts
db_tables: []
depends_on:
- catalog
---

# Web

## Purpose

Serve a local, read-only Fleet dashboard backed directly by Let's federated
index cards. The dashboard makes agent activity inspectable without becoming a
workflow engine or a remote-control surface.

## Public API

| Export | Description |
|--------|-------------|
| `buildFleetSnapshot` | Build a bounded, metadata-only Fleet snapshot from Let catalog APIs. |
| `fleetStateForSessions` | Classify observed heartbeats as Recent activity or History. |
| `fleetHtml` | Render the privacy-bounded local Fleet page. |
| `startFleetWeb` | Start the local-only web server and return its URL and stop handle. |
| `FleetFreshness` | One of live, recent, stale, or unknown activity states. |
| `FleetSession` | Sanitized provider, session label, and bounded freshness metadata. |
| `FleetState` | Recent activity or History repository activity classification. |
| `FleetWorktree` | Sanitized worktree and branch metadata belonging to a repository. |
| `FleetRepository` | Repository-first collection of worktrees and secondary session evidence. |
| `LiveAgent` | Whitelisted local CLI agent with an internal current working directory. |
| `WorkingAgent` | Sanitized live agent record matched to a repo/worktree or marked unassigned. |
| `FleetAgentActivity` | Safe agent-focused status derived from a local process or session metadata. |
| `FleetGitEvidence` | Local Git dirty, ahead/behind, and active Spec Sync change evidence. |
| `gitEvidence` | Read local Git evidence for a resolved agent worktree. |
| `selectAgentWorkContext` | Prefer a bounded child verifier worktree over its agent parent process context. |
| `redactLocalDetail` | Redact known token, credential, and environment-value patterns before local display. |
| `fleetContextLabels` | Build explicit project, worktree, and branch labels for agent supervision. |
| `retainFleetOpenKeys` | Keep open panel keys only while their backing Fleet cards remain available. |
| `mergeFleetOpenKeys` | Merge visible panel state without discarding an open panel in the other view. |
| `fleetLiveChangeAnnouncement` | Describe material agent visibility or status changes for accessible live updates. |
| `parseLocalAgentProcessLines` | Parse only whitelisted agent CLI processes and resolve their cwd internally. |
| `FleetSnapshot` | Sanitized rows, bounded session metadata, refresh interval, and policy. |
| `FLEET_SESSION_ADAPTERS` | Let-native presentation adapters for Claude, Codex, Grok, Gemini, and Antigravity session cards. |
| `fleetAdapterFor` | Resolve the local Fleet presentation adapter for an indexed session card. |
| `fleetSessionDetail` | Extract bounded, redacted display text from an already-indexed local session file. |
| `FleetProvider` | Supported Fleet session-provider name. |
| `FleetSessionDetail` | Bounded local prompt and output detail plus an internal-only path for resolving sanitized project context. |
| `FleetSessionAdapter` | Host-card matching contract for Fleet's presentation adapters. |
| `CORVID_TOKENS_CSS` | Local serving copy of the CorvidLabs design-system token stylesheet. |
| `CORVID_THEME_JS` | Local serving copy of the standard CorvidLabs theme controller. |

## Invariants

1. `let web` binds only to `127.0.0.1` and defaults to port `8731`.
2. Fleet data is derived from Let catalog APIs, not parsed terminal output.
3. Responses never include raw secrets, browser shell endpoints, or agent-control actions. The localhost supervisor may include redacted session text, commands, and project context for active-agent supervision.
4. Repository cards group worktrees first; sessions remain secondary evidence.
5. Working now comes only from a local process probe with exact agent CLI executable allowlist and cwd resolution. Session timestamps never prove a process is running.
6. Unmapped live processes are surfaced as Unassigned running agent and agent type only; their current working directories never leave the process probe.
7. Worktrees, sessions, instructions, and skills are capped before rendering.
8. Session-only records are bounded and include metadata only.
9. The normal CLI exits after ordinary commands, but remains alive while `web`
owns the local server.
10. The agent view distinguishes a process-backed `Working now` status from recent or historical session metadata; no session record is represented as a running process.
11. The process probe may inspect a bounded descendant tree internally to classify an allowlisted operation label; the localhost supervisor may show its redacted command and resolved project context, but never exposes process IDs.
12. The localhost supervisor may expose redacted command, prompt/update, and recent output only after applying `redactLocalDetail`; unavailable detail is labeled instead of inferred.
13. Agent context shows project, worktree, and branch independently when Let can resolve them; raw local paths never leave the server.
14. Fleet maps host session cards through explicit presentation adapters; it does not scan host paths independently of Let's catalog.
15. Gemini must be shown through its Gemini or Antigravity adapter when indexed, otherwise Fleet shows an explicit unavailable state.
16. A 20-second refresh preserves the selected view, open Fleet panels, scroll position, and focused control where the backing card remains available; removed cards may no longer retain state.
17. Material agent visibility or status changes are announced through a polite live region without moving focus.

## Behavioral Examples

```
Given multiple host worktrees and path-only sessions
When let web starts
Then the browser receives sanitized Fleet rows without transcript contents
```

```
Given `let web --port 8732`
When the server starts
Then the URL is http://127.0.0.1:8732 and no non-local interface is bound
```

## Error Cases

| Error | When | Behavior |
|-------|------|----------|
| invalid port | `--port` is not an integer from 1 to 65535 | Return a validation envelope. |
| unavailable port | local port already bound | Let reports Bun's startup failure; no fallback external binding. |

## Dependencies

- `./catalog/context-builder` - trusted scan context.
- `./catalog/find` - structured federated index cards.

## Change Log

| Version | Date | Changes |
|---------|------|---------|
| 1 | 2026-08-01 | Initial local read-only Fleet dashboard. |
44 changes: 44 additions & 0 deletions src/catalog/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,50 @@ export function findCodexSessions(ctx: ScanContext): IndexCard[] {
meta: { source: `codex.${rel}`, level: "child" },
}),
);
// A current Codex session normally lives at year/month/day/rollout.jsonl.
// Index that final file level as well so local Fleet can show its
// already-indexed, redacted latest prompt alongside a live process.
if (isDirectory(child)) {
for (const session of listChildPaths(child, ctx.policy).slice(
0,
50,
)) {
if (!session.endsWith(".jsonl")) {
if (!isDirectory(session)) {
continue;
}
for (const rollout of listChildPaths(
session,
ctx.policy,
Comment on lines +107 to +111

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep Codex session discovery shallow

For a populated Codex history, this adds another nested directory enumeration beneath the existing year, month, and day loops, allowing a large multiplicative scan on every session query and exceeding the repository's shallow-root ScanPolicy invariant. Index a bounded shallow card or use a purpose-built fixed lookup rather than recursively composing listChildPaths.

AGENTS.md reference: AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

).slice(0, 50)) {
if (!rollout.endsWith(".jsonl")) {
continue;
}
cards.push(
makeCard({
kind: "sessions",
host: "codex",
path: rollout,
scope: "user",
pathOnly: true,
meta: { source: `codex.${rel}`, level: "session" },
}),
);
}
continue;
}
cards.push(
makeCard({
kind: "sessions",
host: "codex",
path: session,
scope: "user",
pathOnly: true,
meta: { source: `codex.${rel}`, level: "session" },
}),
);
}
}
}
} else if (name.endsWith(".jsonl")) {
cards.push(
Expand Down
6 changes: 6 additions & 0 deletions src/catalog/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ export async function findAssets(
// Filter before limit so --host kimi is not starved by other hosts
if (opts.host) {
items = items.filter((c) => c.host === opts.host);
// A host-specific session view is an activity feed. Preserve the normal
// catalog sort for federation, but return the newest bounded session cards
// first so a current provider is not hidden by older archive buckets.
if (kind === "sessions") {
items.sort((left, right) => (right.mtime_ms ?? 0) - (left.mtime_ms ?? 0));
}
}

if (opts.query) {
Expand Down
48 changes: 48 additions & 0 deletions src/catalog/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,48 @@ import { geminiHome, projectGeminiDir } from "../paths.ts";
import { instructionId, pathCardId } from "./ids.ts";
import type { IndexCard } from "./types.ts";

export function antigravitySessionCards(
brainRoot: string,
limit: number,
): IndexCard[] {
if (!isDirectory(brainRoot)) {
return [];
}
try {
return readdirSync(brainRoot)
.slice(0, limit)
.flatMap((sessionId) => {
const transcript = join(
brainRoot,
sessionId,
".system_generated",
"logs",
"transcript_full.jsonl",
);
if (!pathExists(transcript)) {
Comment on lines +28 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Confine Antigravity transcript paths to the scan root

If an Antigravity session directory or its transcript is a symlink outside brainRoot, pathExists follows it and this code indexes the external file without applying ScanPolicy's within-root symlink check. Build these cards through the policy-aware child helpers or verify the real transcript remains under the fixed root before indexing it.

AGENTS.md reference: AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

return [];
}
return [
{
id: pathCardId("sessions", transcript),
kind: "sessions" as const,
host: "gemini" as const,
name: `Antigravity session ${sessionId.slice(0, 8)}`,
path: transcript,
scope: "user" as const,
mtime_ms: mtimeMs(transcript),
meta: {
source: "antigravity-cli",
detail_available: true,
},
},
];
});
} catch {
return [];
}
}

function readProjectsMap(): Map<string, string> {
// path -> short name
const map = new Map<string, string>();
Expand Down Expand Up @@ -198,6 +240,12 @@ export function findGeminiSessions(ctx: ScanContext): IndexCard[] {
},
});
}
cards.push(
...antigravitySessionCards(
join(home, "antigravity-cli", "brain"),
ctx.policy.maxEntriesPerRoot,
),
);
}

return cards;
Expand Down
13 changes: 12 additions & 1 deletion src/catalog/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Progressive disclosure: show full body for a card, with security caps.
*/

import { basename } from "node:path";
import { basename, relative } from "node:path";
import { Agent } from "@corvidlabs/agent3md";
import type { ScanContext } from "../adapters/types.ts";
import { LetError } from "../errors.ts";
Expand Down Expand Up @@ -126,6 +126,17 @@ export async function resolveCard(
return nameHits[0];
}
if (nameHits.length > 1) {
// A checkout and its linked worktrees may legitimately contribute the
// same agent.3md plane. When one exact card belongs to the caller's cwd,
// it is the least surprising progressive-disclosure target.
const localHits = nameHits.filter((card) => {
const path = safeRealpath(card.path) ?? card.path;
const rel = relative(ctx.cwd, path);
return rel !== "" && !rel.startsWith("..") && !rel.startsWith("/");
});
if (localHits.length === 1 && localHits[0]) {
return localHits[0];
}
throw new LetError(
"conflict",
`Multiple ${kind} named "${ref}"; pass full id`,
Expand Down
4 changes: 3 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ import { runLet } from "./run.ts";

const result = await runLet(process.argv.slice(2));
process.stdout.write(result.text);
process.exit(result.code);
if (!result.keepAlive) {
process.exit(result.code);
}
4 changes: 3 additions & 1 deletion src/fledge-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ async function main(): Promise<void> {
await new Promise<void>((resolve) => {
process.stdout.write("", () => resolve());
});
process.exit(result.code);
if (!result.keepAlive) {
process.exit(result.code);
}
} catch (err) {
sendError(err instanceof Error ? err.message : String(err));
process.exit(10);
Expand Down
Loading
Loading