CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m19s
CI & Build / Python tests (push) Successful in 2m3s
CI & Build / Build & push image (push) Successful in 18s
The open-after-moment signal proposes a mount; nothing proposed taking one off, so a wrong mount was noise at every occurrence until someone happened to notice. rule_misfired(rule_id, moment, why, reached_by) records a report against a MOUNTED pair, counted per distinct day (the MCP door carries no session id) on a new rule_moment_judgments.misfire column (migration 0119, backup v24). At three days the response carries a line asking the agent to offer the operator the fix - reject takes the rule off, unmap_action stops the action reaching the moment, confirm keeps the mount and stops the asking - and Settings > Moments lists it as an unmount proposal with the reasons and the actions that reached it. A re-mount clears the count. Taught in moments.md, missed-retrieval.md and the reply hold's wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
import { apiGet, apiPost, apiDelete } from "@/api/client";
|
|
|
|
/**
|
|
* The moments of work rules mount on (milestone 458), and the actions that
|
|
* reach each one on this install — `GET /api/retrieval/moments`, the payload
|
|
* the `list_moments` MCP tool returns, plus the counts the Settings view shows
|
|
* beside each moment.
|
|
*/
|
|
export interface Moment {
|
|
name: string;
|
|
/** What is happening at this moment. */
|
|
means: string;
|
|
/** The kinds of action that typically reach it. */
|
|
reached_by: string;
|
|
}
|
|
|
|
/** A shape rather than an entry: `skill.<name>`, one moment per procedure. */
|
|
export interface MomentFamily {
|
|
name: string;
|
|
prefix: string;
|
|
means: string;
|
|
reached_by: string;
|
|
}
|
|
|
|
/** `default` ships with the product; `install` is this install's own. */
|
|
export type ActionVia = "default" | "install";
|
|
|
|
export interface MomentAction {
|
|
tool: string;
|
|
/** Empty = every call of the tool. A command prefix for a command tool,
|
|
* `field=value` pairs for any other. */
|
|
match: string;
|
|
via: ActionVia;
|
|
}
|
|
|
|
/** A shipped default this install switched off. */
|
|
export interface RemovedDefault {
|
|
id: number;
|
|
tool: string;
|
|
match: string;
|
|
moment: string;
|
|
reason: string | null;
|
|
actor: string | null;
|
|
created_at: string | null;
|
|
}
|
|
|
|
export interface MomentUsage {
|
|
/** How many times a mounted rule arrived at this moment. */
|
|
delivered: number;
|
|
/** How many distinct rules did. */
|
|
rules: number;
|
|
/** Of those, how many an agent then opened — an upper bound: a pull records
|
|
* the door, not the line that prompted it. */
|
|
opened: number;
|
|
last_delivered_at: string | null;
|
|
}
|
|
|
|
export interface MomentsPayload {
|
|
moments: Moment[];
|
|
families: MomentFamily[];
|
|
total: number;
|
|
actions: Record<string, MomentAction[]>;
|
|
removed_defaults: RemovedDefault[];
|
|
/** Rules mounted per moment; a moment carrying none is absent. */
|
|
mounted: Record<string, number>;
|
|
/** The count read failed — `mounted` is empty for that reason, not because
|
|
* nothing is mounted. */
|
|
mounted_failed?: boolean;
|
|
usage: {
|
|
by_moment: Record<string, MomentUsage>;
|
|
days: number;
|
|
/** Same distinction as `mounted_failed`, for the usage read. */
|
|
moment_usage_failed?: boolean;
|
|
};
|
|
}
|
|
|
|
export interface MappingChange {
|
|
tool: string;
|
|
match?: string;
|
|
moment: string;
|
|
reason?: string;
|
|
}
|
|
|
|
export function getMoments(days = 30): Promise<MomentsPayload> {
|
|
return apiGet(`/api/retrieval/moments?days=${days}`);
|
|
}
|
|
|
|
export function mapAction(change: MappingChange): Promise<unknown> {
|
|
return apiPost("/api/retrieval/moments/mappings", change);
|
|
}
|
|
|
|
/** Query parameters, not a body: DELETE bodies are not reliably sent. */
|
|
export function unmapAction(change: MappingChange): Promise<void> {
|
|
const q = new URLSearchParams({
|
|
tool: change.tool,
|
|
match: change.match ?? "",
|
|
moment: change.moment,
|
|
reason: change.reason ?? "",
|
|
});
|
|
return apiDelete(`/api/retrieval/moments/mappings?${q.toString()}`);
|
|
}
|
|
|
|
/**
|
|
* Proposals about a rule's moments, waiting on a person. A `mount` proposal
|
|
* (milestone 458 step 7) comes from a pass that read the rule, or from the
|
|
* rule being opened just after the moment fired. An `unmount` proposal (step
|
|
* 7b) comes from sessions reporting that the mount arrived where it did not
|
|
* apply. `GET /api/retrieval/moments/proposals`, the payload the
|
|
* `rule_moment_proposals` MCP tool returns.
|
|
*/
|
|
export type ProposalSource = "pass" | "signal" | "edit" | "misfire";
|
|
|
|
export interface MisfireReason {
|
|
why: string;
|
|
reached_by: string;
|
|
at: string;
|
|
}
|
|
|
|
export interface MomentProposal {
|
|
proposal: "mount" | "unmount";
|
|
moment: string;
|
|
source: ProposalSource;
|
|
why: string;
|
|
/** For a misfire, `situations` counts distinct days and `co_surfaced` the reports. */
|
|
evidence: { situations: number; projects: number; co_surfaced: number };
|
|
created_at: string | null;
|
|
/** Unmount proposals only: the newest reasons given, and how often each action reached the moment. */
|
|
reasons?: MisfireReason[];
|
|
reached_by?: Record<string, number>;
|
|
}
|
|
|
|
export interface RuleProposals {
|
|
id: number;
|
|
title: string;
|
|
kind: string;
|
|
statement: string;
|
|
when_to_apply: string;
|
|
home: "global" | "project";
|
|
/** What the rule is mounted on now. */
|
|
mounted: string[];
|
|
proposals: MomentProposal[];
|
|
}
|
|
|
|
export interface ProposalsPayload {
|
|
rules: RuleProposals[];
|
|
total: number;
|
|
}
|
|
|
|
export type Verdict = "confirm" | "reject";
|
|
|
|
export interface MomentJudgment {
|
|
rule_id: number;
|
|
moment: string;
|
|
verdict: Verdict;
|
|
note?: string;
|
|
}
|
|
|
|
export interface JudgeResult {
|
|
judged: { rule_id: number; moment: string; state: string }[];
|
|
refused: { rule_id: number; moment?: string; error: string }[];
|
|
}
|
|
|
|
export function getProposals(ruleId?: number): Promise<ProposalsPayload> {
|
|
return apiGet(`/api/retrieval/moments/proposals${ruleId ? `?rule_id=${ruleId}` : ""}`);
|
|
}
|
|
|
|
/**
|
|
* A confirm MOUNTS the rule on the moment (on an unmount proposal, keeps it);
|
|
* a reject unmounts it if mounted and stops the pair being proposed again.
|
|
*/
|
|
export function judgeProposals(judgments: MomentJudgment[]): Promise<JudgeResult> {
|
|
return apiPost("/api/retrieval/moments/proposals/judge", { judgments });
|
|
}
|