src/ops-types/feed.ts
330 lines and 0 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
None: it only declares types or values.
Imports
It imports src/ops-types/sites.ts. Imported by src/inbox.ts, src/merge-safety.ts, src/merge-units.ts, src/ops-feed.ts, src/ops-types/portal.ts, src/shared-code.ts, src/status.ts, src/task-runs.ts, src/watcher-digest.ts.
Source
import type { OpsSiteConfig, OpsPackageConfig, OpsSnapshot } from "./sites";
export type OpsJobStatus = "queued" | "claimed" | "blocked" | "done" | "failed" | "superseded" | "parked";
export interface OpsJob {
id: string;
namespace: string;
title: string;
status: OpsJobStatus;
priority: number;
posted_by: string;
claimed_by: string | null;
created_at: string;
// The last transition: there is no blocked_at or done_at column.
updated_at: string;
blocked_count: number;
resumed_count: number;
gate_required: boolean;
// For a blocked job: the reason and the exact command, split out of result_summary
// (commandFromSummary). Null otherwise.
waits_on: string | null;
command: string | null;
// Whether that command is the one the holder's block wrote (src/job-signing.ts):
// "mismatch" withholds it (command is null), "legacy-unsigned" is a block from before
// signing. Null for a job that is not blocked.
command_signature: "verified" | "legacy-unsigned" | "mismatch" | "unconfigured" | null;
// A blocked job that asks something (capsid_work_jobs action block with question: true), answered by a
// resume note, rather than one waiting on a command to be run. Optional so a feed written
// before it, and the app's sample data, stay valid.
question?: boolean;
// A parked job (src/jobs-park.ts): the one line saying when it comes back. Absent for
// every other status, and in a feed written before parked jobs were shown.
parked_reason?: string;
// The started label (src/jobs-begin.ts): who began a job worked without a claim, and
// when. Absent for a job nobody began, and in a feed written before the label existed.
started_by?: string;
started_at?: string;
result_ref: string | null;
// Posted by agent:watcher, with its fingerprint from the title, and how often the
// watcher has seen it (seen_count, last_seen) while this is the finding's current
// job; both null otherwise (src/watcher-findings.ts).
finding: { fingerprint: string; seen_count: number | null; last_seen: string | null } | null;
}
export interface OpsAgent {
name: string;
kind: string;
namespaces: "*" | string[];
flags: string[];
last_seen: string | null;
revoked_at: string | null;
// When the watcher pass moved it to the archive (src/agents-archive.ts); null while
// its row is in agents. The Agents roster lists rows with neither date set.
archived_at: string | null;
jobs_done: number;
jobs_failed: number;
jobs_blocked: number;
prs_opened: number;
prs_merged: number;
pr_merge_rate: number | null;
ci_green_rate: number | null;
median_duration_minutes: number | null;
}
export interface OpsPr {
job_id: string;
pr_url: string;
merged: boolean | null;
merge_verified_at: string | null;
recorded_at: string;
}
export interface OpsAwaitingSeat {
namespace: string;
repo: string;
number: number;
failed: string;
why: string;
at: string;
// The merge pipeline's class, report-only (src/merge-class.ts); absent on older sets.
class?: "auto" | "approve" | "typed" | "seat" | "wait" | null;
path_class?: string | null;
class_reasons?: string[];
// The head the tick judged; the Portal's Approve sends it as the sha approved.
head_sha?: string;
// A pull request labelled visual (src/visual-pr.ts): its title, the live preview its
// description names, and its screenshots, all read from the pull request itself.
visual?: { title: string; preview: string | null; shots: string[] };
}
// The overnight run's switch (src/overnight.ts). decision is present only while the mode is
// subscription: who decided, when, the ruling and its reasoning, and who set the switch.
interface OpsOvernight {
mode: "off" | "api" | "subscription";
decision: {
decided_by: string;
decided_on: string;
ruling: string;
reasoning: string;
set_by: string;
set_at: string;
reason: string;
} | null;
}
// A Claude Code session Capsid has heard from through its hooks (POST /ops/hooks,
// src/ops-hooks.ts). Summaries only: no prompt, response or tool content is kept.
export interface OpsSession {
session_id: string;
// The key that reported it, as an actor string (agent:<name>).
agent: string;
// The job it was first bound to, and that job's namespace; null when the key held no
// single job at its first event.
job_id: string | null;
namespace: string | null;
// SessionStart's source (startup, resume, clear, compact, fork) and model.
source: string | null;
model: string | null;
permission_mode: string | null;
started_at: string;
last_event_at: string;
// The newest hook event's name, and the newest Notification's type.
last_event: string;
last_notification_type: string | null;
// Waiting on a person: a permission prompt, an idle prompt, an elicitation dialog.
needs_input: boolean;
// The newest StopFailure's error (rate_limit, billing_error, ...), until a turn ends
// normally.
last_failure: string | null;
// Why the session is an incident now, decided when the feed is read (no job is
// posted): "failure" for a StopFailure a person must act on (rate_limit,
// billing_error, authentication_failed, account_on_hold, oauth_org_not_allowed),
// "waiting" for needing input more than ten minutes. Null otherwise.
incident: "failure" | "waiting" | null;
}
// Shared code (src/shared-code.ts): GET /portal/api/shared-code.
export interface SharedCodeApp {
namespace: string;
name: string;
// The manifest it was read from ("package.json", "dashboard/package.json") or "renovate".
where: string;
// The tag the app pins, or the lockfile's version for a spec with no tag; null when
// neither says (a branch).
pinned: string | null;
// Release tags newer than the pin, or null when the pin is not a release.
behind: number | null;
// Pinned through a repo's earlier name (GitHub redirects it).
via_old_name: boolean;
}
export interface SharedCodePackage {
name: string;
repo: string;
latest: string | null;
tags_error: string | null;
// The version in the package repo's root package.json on its default branch; null when
// it has none or the file was not read (manifest_error says why).
version: string | null;
// True when the tags were read and none names that version: the daily maintenance pass
// tags it (src/maintenance-releases.ts). False when it is tagged or this is unknown.
untagged: boolean;
// "private": true in that package.json: never published, so npm is not asked.
private: boolean;
// npm's latest dist-tag, or null with npm_note saying why (not on npm, unread); both
// null for a private package.
npm_latest: string | null;
npm_note: string | null;
manifest_error: string | null;
users: SharedCodeApp[];
// Users at least one release behind.
behind: number;
// Known local copies the package replaces; present is null when that app's tree could not be read.
local_copies: Array<{ namespace: string; path: string; present: boolean | null }>;
local_left: number;
}
export interface SharedCodeView {
generated: string;
configured: boolean;
error: string | null;
// How many apps were read, so "nobody is behind" is never "nothing was read".
apps_read: number;
apps_failed: Array<{ namespace: string; error: string }>;
packages: SharedCodePackage[];
}
// What needs Dustin, per app (src/inbox.ts): GET /ops/inbox, capsid_run_improve action
// status's needs_dustin, and the Portal's Needs you page all carry this one shape.
export type InboxSeverity = "needs-you" | "failing" | "none";
// "proposal": a merge unit promotion, demotion or suspension (src/merge-units.ts).
export type InboxKind = "blocked-job" | "question" | "pr" | "ci" | "site-down" | "report" | "proposal";
export interface InboxItem {
title: string;
kind: InboxKind;
/** Where to act on it, or null when there is no page for it. */
link: string | null;
/** When it started waiting, ISO. The shell shows the age. */
since: string;
/** The job it is, for a blocked job or a question, so the Portal opens the job's panel
* when there is no link. Absent on every other kind. */
job?: string;
}
export interface InboxApp {
namespace: string;
name: string;
count: number;
severity: InboxSeverity;
items: InboxItem[];
}
export interface Inbox {
generated: string;
/** Items across the apps this caller may read. */
count: number;
severity: InboxSeverity;
apps: InboxApp[];
}
// One namespace's last seven days of auto-merge, the safety meter (src/merge-safety.ts).
// approved: merges Dustin approved. bad_auto_7d: rollbacks and reverts of pull requests
// the tick merged unattended. paused: the shutoff, with when and why, or null.
export interface OpsMergeSafetyRow {
namespace: string;
auto_merged: number;
approved: number;
deploys_verified: number;
rolled_back: number;
rollback_failed: number;
reverts: number;
bad_auto_7d: number;
paused: { since: string; reason: string } | null;
}
export interface OpsMergeSafety {
generated: string;
namespaces: OpsMergeSafetyRow[];
}
type OpsMergePolicy = { loads: true; version: string; enabled: boolean } | { loads: false; reason: string };
export interface OpsLive {
generated: string;
// Every open job, and every job that ended in the last 24 hours.
jobs: OpsJob[];
agents: OpsAgent[];
// Pull requests recorded against jobs in the last 7 days.
prs: OpsPr[];
awaiting_seat: OpsAwaitingSeat[];
// The auto-merge safety meter the tick's merge-safety step kept, or null before its
// first pass. One KV get, no D1 or GitHub read.
merge_safety: OpsMergeSafety | null;
// Whether the signed auto-merge policy loads as the tick loads it (loadMergePolicy), and
// if not why, so the Portal's policy panel can say so beside its sign-from-repo button.
merge_policy: OpsMergePolicy;
overnight: OpsOvernight;
// Sessions with no SessionEnd whose last event was in the last 24 hours, newest
// first, at most 50.
sessions: OpsSession[];
// Every app roster namespace (APP_ROSTER in src/roster.ts), read from code. The
// per-namespace detail is GET /portal/api/namespaces, read when the Namespaces view
// opens.
namespaces: Array<{ name: string }>;
// The site configuration, every row. With no row that has an origin, the Portal
// shows no Sites view and no site items on the Overview.
sites: OpsSiteConfig[];
// The package configuration, every row. With none, the Portal shows no Packages view.
packages: OpsPackageConfig[];
// The D1 store's size, from the jobs read's meta.size_after (null when D1 did not
// report it), against the per-database cap (D1_CAP_BYTES in src/ops-feed.ts).
store: { size_bytes: number | null; cap_bytes: number };
// Drivers' writes to canon documents waiting for approval (src/canon.ts), oldest
// first, at most 50.
canon_proposals: OpsCanonProposal[];
// What needs Dustin, per app, from the same gatherer GET /ops/inbox answers with
// (src/inbox.ts), unrestricted: the Portal is the admin's.
inbox: Inbox;
// The watcher's open findings that post no job (src/watcher-checks.ts, findingClass): class
// digest feeds the weekly digest per site, class attention the attention list.
findings: OpsFinding[];
}
export interface OpsFinding {
fingerprint: string;
namespace: string;
class: "digest" | "attention";
headline: string;
first_seen: string;
last_seen: string;
seen_count: number;
// The newest sighting's evidence lines, at most three.
evidence: string[];
}
// One pending canon proposal, measured against the document as it is now. stale: the
// document moved past the body the proposal was written against, so it can only be
// rejected. directive_lines: the added lines that read as instructions to agents.
export interface OpsCanonProposal {
id: number;
namespace: string;
path: string;
proposer: string;
created_at: string;
creates: boolean;
stale: boolean;
added: number;
removed: number;
directive_lines: string[];
}
// The run ledger (src/task-runs.ts): each scheduled task's newest runs, and its flag
// decided when the feed was read. failing: the newest run threw or was refused.
// quiet: a periodic task with no run in twice its period. never: no run recorded.
export type OpsTaskRunOutcome = "ok" | "skipped" | "refused" | "threw";
export interface OpsTaskRun {
started_at: string;
finished_at: string;
outcome: OpsTaskRunOutcome;
// One line: what the run did, or why it did not.
reason: string;
}
export interface OpsTask {
id: string;
label: string;
// Null for a task whose runs follow the work, which is flagged only when it fails.
period_ms: number | null;
flag: "failing" | "quiet" | "never" | null;
// Newest first, at most five.
recent: OpsTaskRun[];
}
export interface OpsFeed {
// Null until the watcher has written its first pass.
snapshot: OpsSnapshot | null;
live: OpsLive;
// The run ledger, or why it could not be read.
scheduled: { tasks: OpsTask[]; error: null } | { tasks: null; error: string };
// When the next on-demand pass is allowed (POST /portal/api/ops/refresh), or null
// when one is allowed now.
refresh_allowed_at: string | null;
// Whether CF_OPS_TOKEN is set, so the app can say why deploy and error columns are
// empty.
cloudflare_configured: boolean;
// The double-submit CSRF value: the same value is in the HttpOnly cookie
// capsid_portal_csrf, and the app sends it back as X-Capsid-CSRF on every action. A
// cross-site page cannot read this body, so it cannot learn the value.
csrf: string;
// Who is signed in, for the top bar: from the session, never from configuration.
user: { name: string; initials: string };
}