Merge gitea/main into the Motion branch

Motion was written against a base five commits behind main, so the
integration is the interesting part of this commit:

- The migration is renumbered 0014 -> 0015. Main shipped
  0014_piggy_conversations, and two migrations sharing an index is a
  journal that applies one of them.
- The seed-idempotency gate keeps main's all-tables diff rather than the
  motion_templates counter this branch added; the general check subsumes
  the specific one.
- Nav gains a Motion group alongside main's new Workspace group, and
  Piggy keeps the mark main gave it.
- Stat keeps main's container-scaled figure, which already carries the
  min-w-0 this branch added for the same reason.
- Piggy's page labels keep main's refusal wording for the four pages with
  no tool of their own, and gain the three Motion routes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 18:30:45 -07:00
149 changed files with 37440 additions and 3502 deletions
+1
View File
@@ -5,6 +5,7 @@ export * from './margin';
export * from './motion';
export * from './permissions';
export * from './piggy-context';
export * from './piggy-protocol';
export * from './theme';
export * from './imports';
export * from './lifecycle';
+153
View File
@@ -0,0 +1,153 @@
/**
* The wire contract between the browser, the API relay and the Piggy agent.
*
* Like `piggy-context`, this crosses process boundaries and is validated
* `.strict()` at two of them, so it lives here once and every hop derives from
* it. Unlike `piggy-context`, it also has to survive a harness swap: the events
* below are PIG's own vocabulary, deliberately NOT Prime Agent's. The agent
* runtime emits `message_update` / `tool_execution_start` / `turn_end` and a
* dozen more; the chat server narrows that to the eight cases the product
* actually renders. Keeping the translation on the server means a harness
* upgrade is a server change, not a client one.
*
* Two things here are new to the agent era and worth stating plainly:
*
* approval — a write tool in `confirm` mode does not perform its mutation. It
* returns a description of what it WOULD do and yields an
* `approval_required` event; the write happens only when the user
* answers. The model is told the write is pending, not done, so it
* cannot report success it has not achieved.
* model — which model answered is part of the record. It varies per turn
* now that the user can choose, so it rides on the events rather
* than being read from configuration.
*/
/**
* How far Piggy may act without being asked again.
*
* `read_only` keeps the pre-agent behaviour: no write tool is even offered to
* the model, which is a stronger guarantee than offering one and refusing it.
*/
export const PIGGY_MODES = ['read_only', 'confirm', 'auto'] as const;
export type PiggyMode = (typeof PIGGY_MODES)[number];
/** Writes that always need a human, whatever the mode. */
export const PIGGY_ALWAYS_CONFIRM_KINDS = [
'contract',
'commitment',
'allocation',
'compliance',
] as const;
export type PiggyGuardedKind = (typeof PIGGY_ALWAYS_CONFIRM_KINDS)[number];
/**
* A model the user may choose, as the UI needs it.
*
* `costPerMTokIn`/`Out` are US dollars per million tokens — NOT cents. This is
* the one money field in PIG that is not an integer of cents, because that is
* the unit every provider publishes and converting it here would invite the
* same 100x error the units rule exists to prevent. The field names say so.
*/
export interface PiggyModelOption {
/** Provider-qualified id, e.g. `nvidia/nemotron-3-nano-30b-a3b`. */
id: string;
label: string;
/** Short note on when to reach for it, shown under the label. */
hint?: string;
costPerMTokIn: number;
costPerMTokOut: number;
contextWindow: number;
/** True when the model supports a reasoning budget. */
reasoning: boolean;
/** The default the deployment ships with, when no user preference is stored. */
isDefault?: boolean;
}
/** A change Piggy proposes but has not made. */
export interface PiggyProposedChange {
/** Stable within a turn; the client answers with it. */
id: string;
/** The tool that proposed it, e.g. `pig_log_activity`. */
tool: string;
kind: string;
/** One line, in the user's language: "Log a call on Northwind Robotics". */
summary: string;
/** Field-level detail for the diff card. Values are already display-formatted. */
fields: { label: string; value: string; previous?: string }[];
/** Set when the change targets an existing record the user can open. */
record?: { type: string; id: string; label?: string };
/** True when the mode would have applied this automatically but policy forbade it. */
forcedConfirm?: boolean;
}
export type PiggyApprovalDecision = 'apply' | 'reject';
/**
* Events the chat server streams as NDJSON.
*
* `content_delta` and `reasoning_delta` are unchanged from the pre-agent
* protocol so the transcript renderer did not have to be rewritten around the
* harness swap.
*/
export type PiggyChatEvent =
| { type: 'meta'; model: string; mode: PiggyMode; conversationId: string }
| { type: 'reasoning_delta'; delta: string }
| { type: 'content_delta'; delta: string }
| { type: 'tool_call'; id: string; name: string; arguments: unknown }
| {
type: 'tool_result';
id: string;
name: string;
ok: boolean;
result?: unknown;
error?: string;
}
/** A write is waiting on the user. The turn stays open until it is answered. */
| { type: 'approval_required'; change: PiggyProposedChange }
/** The outcome of an answered approval, so the transcript can settle the card. */
| {
type: 'approval_resolved';
changeId: string;
decision: PiggyApprovalDecision;
ok: boolean;
error?: string;
}
| {
type: 'done';
inputTokens: number | null;
outputTokens: number | null;
/** Whole US cents spent on this turn, when the provider reported usage. */
costMicroCents: number | null;
/** `length` when the answer was cut short by the token budget. */
finishReason?: string;
}
| { type: 'error'; message: string; code?: string; retryAfterSeconds?: number };
export type PiggyChatEventType = PiggyChatEvent['type'];
/** A stored conversation, as the workspace sidebar lists them. */
export interface PiggyConversationSummary {
id: string;
title: string;
updatedAt: string;
messageCount: number;
/** Present while a turn is still streaming. */
running?: boolean;
}
export function isGuardedKind(kind: string): kind is PiggyGuardedKind {
return (PIGGY_ALWAYS_CONFIRM_KINDS as readonly string[]).includes(kind);
}
/**
* Whether a proposed change may be applied without asking.
*
* Stated as one function, used by the agent runtime AND asserted by the tests,
* so "auto mode still stops at a contract" cannot drift into being true in one
* place and false in another.
*/
export function requiresApproval(mode: PiggyMode, kind: string): boolean {
if (mode === 'read_only') return true;
if (mode === 'confirm') return true;
return isGuardedKind(kind);
}