chore: merge develop into work/companion

This commit is contained in:
2026-08-21 11:38:47 +09:00
34 changed files with 1124 additions and 319 deletions
@@ -135,6 +135,11 @@ export type SubscriptionWorker = { worker_id: SubscriptionWorkerId,
* Runtime producers leave this unset because the connection identifies the Runtime.
*/
runtime_id?: string | null,
/**
* Workspace-scoped canonical resource key. Runtime producers leave this unset;
* Workspace-facing projections must populate it before publishing the Worker.
*/
resource_key?: string | null,
/**
* Producer-owned monotonic revision for this Worker subject.
*/
@@ -5,7 +5,7 @@ export type InvalidProjectRecord = { label: string; reason: string };
export type TicketSummary = {
id: string;
human_key: string;
resource_key: string;
title: string;
state: string;
priority: string;
@@ -54,7 +54,7 @@ export type TicketEventDetail = {
export type ObjectiveLinkSummary = {
id: string;
human_key: string;
resource_key: string;
title: string;
state: string;
};
@@ -72,7 +72,7 @@ export type TicketAssignmentSummary = {
assignment_id: string;
runtime_id: string;
worker_id: string;
worker_human_key?: string | null;
worker_resource_key?: string | null;
};
export type TicketMergeRequestSummary = {
@@ -133,7 +133,7 @@ export type TicketQueryRequest = {
export type TicketQueryItem = {
id: string;
human_key: string;
resource_key: string;
title: string;
state: string;
readiness: string | null;
@@ -169,7 +169,7 @@ export type TicketRelation = {
ticket_id: string;
kind: string;
target: string;
target_human_key?: string | null;
target_resource_key?: string | null;
note: string | null;
author: string;
at: string;
@@ -177,7 +177,7 @@ export type TicketRelation = {
export type DerivedTicketRelation = {
source_ticket: string;
source_human_key?: string | null;
source_resource_key?: string | null;
inverse_kind: string;
forward_kind: string;
note: string | null;
@@ -187,7 +187,7 @@ export type DerivedTicketRelation = {
export type TicketRelationBlocker = {
blocking_ticket: string;
blocking_human_key?: string | null;
blocking_resource_key?: string | null;
reason_kind: string;
relation_kind: string;
note: string | null;
@@ -209,7 +209,7 @@ export type TicketRelationView = {
export type TicketDetail = {
id: string;
human_key: string;
resource_key: string;
title: string;
state: string;
readiness: string | null;
@@ -1,7 +1,7 @@
const HUMAN_KEY_PATTERN = /^(T|O|W)-(\d+)/;
const RESOURCE_KEY_PATTERN = /^(T|O|W)-(\d+)/;
export function resourceHumanKey(reference: string): string {
const match = HUMAN_KEY_PATTERN.exec(reference);
export function resourceKey(reference: string): string {
const match = RESOURCE_KEY_PATTERN.exec(reference);
return match ? `${match[1]}-${match[2]}` : reference;
}
@@ -18,32 +18,30 @@ export function slugifyResourceTitle(title: string): string {
}
export function canonicalResourceReference(
humanKey: string,
resourceKey: string,
title: string,
): string {
return `${humanKey}-${slugifyResourceTitle(title)}`;
return `${resourceKey}-${slugifyResourceTitle(title)}`;
}
export function ticketHref(
workspaceId: string,
ticket: { human_key: string; title: string },
ticket: { resource_key: string; title: string },
): string {
return `/w/${encodeURIComponent(workspaceId)}/tickets/${encodeURIComponent(canonicalResourceReference(ticket.human_key, ticket.title))}`;
return `/w/${encodeURIComponent(workspaceId)}/tickets/${encodeURIComponent(canonicalResourceReference(ticket.resource_key, ticket.title))}`;
}
export function objectiveHref(
workspaceId: string,
objective: { human_key: string; title: string },
objective: { resource_key: string; title: string },
): string {
return `/w/${encodeURIComponent(workspaceId)}/objectives/${encodeURIComponent(canonicalResourceReference(objective.human_key, objective.title))}`;
return `/w/${encodeURIComponent(workspaceId)}/objectives/${encodeURIComponent(canonicalResourceReference(objective.resource_key, objective.title))}`;
}
export function workerHref(
workspaceId: string,
worker: { human_key?: string; display_name: string; worker_id: string },
worker: { resource_key: string; display_name: string },
): string {
const reference = worker.human_key
? canonicalResourceReference(worker.human_key, worker.display_name)
: worker.worker_id;
const reference = canonicalResourceReference(worker.resource_key, worker.display_name);
return `/w/${encodeURIComponent(workspaceId)}/workers/${encodeURIComponent(reference)}`;
}
@@ -76,7 +76,7 @@ export type WorkerCapabilities = {
export type Worker = {
runtime_id: string;
worker_id: string;
human_key?: string;
resource_key: string;
host_id: string;
display_name: string;
label: string;
@@ -404,7 +404,7 @@ export type {
export type ObjectiveSummary = {
id: string;
human_key: string;
resource_key: string;
title: string;
state: string;
updated_at?: string | null;
@@ -415,14 +415,14 @@ export type ObjectiveSummary = {
export type ObjectiveLinkedTicketSummary = {
id: string;
human_key: string;
resource_key: string;
title: string;
state: string;
};
export type ObjectiveDetail = {
id: string;
human_key: string;
resource_key: string;
title: string;
state: string;
created_at?: string | null;
@@ -74,10 +74,12 @@ export function workspaceWorkersStore(workspaceId: string): Readable<WorkspaceWo
function projectWorker(worker: SubscriptionWorker): SidebarWorker {
if (!worker.runtime_id) throw new Error('Workspace Worker projection is missing runtime_id');
if (!worker.resource_key) throw new Error('Workspace Worker projection is missing resource_key');
const displayName = worker.display_name ?? `Worker ${worker.worker_id}`;
return {
runtime_id: worker.runtime_id,
worker_id: worker.worker_id,
resource_key: worker.resource_key,
host_id: worker.runtime_id,
display_name: displayName,
label: displayName,
@@ -19,6 +19,7 @@ function worker(overrides: Partial<Worker>): Worker {
return {
runtime_id: "arc",
worker_id: "1",
resource_key: "W-1",
host_id: "host",
display_name: "Worker 1",
label: "Worker 1",
@@ -53,7 +53,7 @@ export type TicketLaneDefinition = (typeof LANE_DEFINITIONS)[number];
export type TicketLaneId = TicketLaneDefinition["id"];
export type TicketCardSummary = Pick<
TicketSummary,
"id" | "human_key" | "title" | "state" | "priority" | "updated_at"
"id" | "resource_key" | "title" | "state" | "priority" | "updated_at"
>;
const STATE_SORT_ORDER = new Map<string, number>([
@@ -30,7 +30,7 @@
<div class="objective-meta" aria-label="Objective metadata">
<span>Updated {objective.updated_at ? formatDate(objective.updated_at) : 'unknown'}</span>
<span>{objective.linked_tickets?.length ? `${objective.linked_tickets.length} linked ticket(s)` : 'No linked tickets'}</span>
<code>{objective.human_key}</code>
<code>{objective.resource_key}</code>
</div>
</a>
{/each}
@@ -25,7 +25,7 @@
</div>
<div class="objective-meta" aria-label="Objective metadata">
<span>Updated {objective.updated_at ? formatDate(objective.updated_at) : 'unknown'}</span>
<code>{objective.human_key}</code>
<code>{objective.resource_key}</code>
</div>
</a>
{/each}
@@ -64,7 +64,7 @@
<dd>
{#if data.objective.linked_ticket_summaries.length}
{#each data.objective.linked_ticket_summaries as ticket, index}
{#if index}, {/if}<a href={ticketHref(data.workspaceId, ticket)}>{ticket.human_key}</a>
{#if index}, {/if}<a href={ticketHref(data.workspaceId, ticket)}>{ticket.resource_key}</a>
{/each}
{:else}
none
@@ -2,7 +2,7 @@ import { redirect } from "@sveltejs/kit";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import {
canonicalResourceReference,
resourceHumanKey,
resourceKey,
} from "$lib/workspace/resource-links";
import type {
ObjectiveDetail,
@@ -12,7 +12,7 @@ import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const apiPath = (path: string) => workspaceApiPath(params.workspaceId, path);
const objectiveId = resourceHumanKey(params.objectiveId);
const objectiveId = resourceKey(params.objectiveId);
const [objectives, objective] = await Promise.all([
loadJson<ObjectiveListResponse>(fetch, apiPath("/objectives")),
loadJson<ObjectiveDetail>(
@@ -23,7 +23,7 @@ export const load: PageLoad = async ({ fetch, params }) => {
if (objective.data) {
const canonical = canonicalResourceReference(
objective.data.human_key,
objective.data.resource_key,
objective.data.title,
);
if (params.objectiveId !== canonical) {
@@ -179,7 +179,7 @@
class="ticket-card"
href={ticketHref(data.workspaceId, ticket)}
>
<span class="ticket-card-id">{ticket.human_key}</span>
<span class="ticket-card-id">{ticket.resource_key}</span>
<strong>{ticket.title}</strong>
<div class="ticket-card-meta">
<span>{ticket.state} · {ticket.priority}</span>
@@ -255,27 +255,42 @@
{#if ticket.relations.blockers.length > 0}
<div class="ticket-blocker-list">
{#each ticket.relations.blockers as blocker}
<a href={`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(blocker.blocking_human_key ?? blocker.blocking_ticket)}`}>
<strong>Blocked by {blocker.blocking_human_key ?? blocker.blocking_ticket}</strong>
<span>{relationLabel(blocker.relation_kind)} · {blocker.blocking_state}</span>
</a>
{#if blocker.blocking_resource_key}
<a href={`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(blocker.blocking_resource_key)}`}>
<strong>Blocked by {blocker.blocking_resource_key}</strong>
<span>{relationLabel(blocker.relation_kind)} · {blocker.blocking_state}</span>
</a>
{:else}
<div>
<strong>Blocked by resource key unavailable</strong>
<span>{relationLabel(blocker.relation_kind)} · {blocker.blocking_state}</span>
</div>
{/if}
{/each}
</div>
{/if}
<div class="ticket-relations-list">
{#each ticket.relations.outgoing as relation}
<a href={`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(relation.target_human_key ?? relation.target)}`}>
<span>{relationLabel(relation.kind)}</span>
<strong>{relation.target_human_key ?? relation.target}</strong>
{#if relation.note}<small>{relation.note}</small>{/if}
</a>
{#if relation.target_resource_key}
<a href={`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(relation.target_resource_key)}`}>
<span>{relationLabel(relation.kind)}</span>
<strong>{relation.target_resource_key}</strong>
{#if relation.note}<small>{relation.note}</small>{/if}
</a>
{:else}
<span><strong>resource key unavailable</strong></span>
{/if}
{/each}
{#each ticket.relations.incoming as relation}
<a href={`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(relation.source_human_key ?? relation.source_ticket)}`}>
<span>{relationLabel(relation.inverse_kind)}</span>
<strong>{relation.source_human_key ?? relation.source_ticket}</strong>
{#if relation.note}<small>{relation.note}</small>{/if}
</a>
{#if relation.source_resource_key}
<a href={`/w/${encodeURIComponent(data.workspaceId)}/tickets/${encodeURIComponent(relation.source_resource_key)}`}>
<span>{relationLabel(relation.inverse_kind)}</span>
<strong>{relation.source_resource_key}</strong>
{#if relation.note}<small>{relation.note}</small>{/if}
</a>
{:else}
<span><strong>resource key unavailable</strong></span>
{/if}
{/each}
{#if ticket.relations.outgoing.length === 0 && ticket.relations.incoming.length === 0}
<p class="workspace-empty-copy">No Ticket relations.</p>
@@ -2,7 +2,7 @@ import { redirect } from "@sveltejs/kit";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import {
canonicalResourceReference,
resourceHumanKey,
resourceKey,
} from "$lib/workspace/resource-links";
import type { WorkspaceOrchestratorStatus } from "$lib/workspace/tickets/ticket-panel";
import type {
@@ -12,7 +12,7 @@ import type {
import type { PageLoad } from "./$types";
export const load = (async ({ fetch, params }) => {
const reference = resourceHumanKey(params.ticketId);
const reference = resourceKey(params.ticketId);
const ticketPath = workspaceApiPath(
params.workspaceId,
`/tickets/${encodeURIComponent(reference)}`,
@@ -30,7 +30,7 @@ export const load = (async ({ fetch, params }) => {
]);
if (ticket.data) {
const canonical = canonicalResourceReference(
ticket.data.human_key,
ticket.data.resource_key,
ticket.data.title,
);
if (params.ticketId !== canonical) {
@@ -194,12 +194,12 @@
{@const workerDisplayName = worker.display_name || worker.label}
<tr>
<td>
{#if canOpenWorkerConsole(worker)}
<a class="worker-title-link" href={workerHref(data.workspaceId, worker)}><strong>{workerDisplayName}</strong></a>
{#if canOpenWorkerConsole(worker) && worker.resource_key}
<a class="worker-title-link" href={workerHref(data.workspaceId, { ...worker, resource_key: worker.resource_key })}><strong>{workerDisplayName}</strong></a>
{:else}
<strong>{workerDisplayName}</strong>
{/if}
<small>worker <code>{worker.human_key ?? worker.worker_id}</code></small>
<small>worker <code>{worker.resource_key}</code></small>
</td>
<td><code>{worker.runtime_id}</code></td>
<td>{workerProfile(worker)}</td>
@@ -4,7 +4,7 @@
let { data }: { data: PageData } = $props();
</script>
<svelte:head><title>{data.worker?.human_key ?? 'Worker'} · Yoi</title></svelte:head>
<svelte:head><title>{data.worker?.resource_key ?? 'Worker'} · Yoi</title></svelte:head>
<section class="workspace-page-shell">
{#if data.workerError}
@@ -12,7 +12,7 @@
{:else if data.worker}
<header class="workspace-page-header">
<div>
<p class="eyebrow">{data.worker.human_key}</p>
<p class="eyebrow">{data.worker.resource_key}</p>
<h1>{data.worker.display_name}</h1>
</div>
<a
@@ -2,13 +2,13 @@ import { redirect } from "@sveltejs/kit";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import {
canonicalResourceReference,
resourceHumanKey,
resourceKey,
} from "$lib/workspace/resource-links";
import type { Worker } from "$lib/workspace/sidebar/types";
import type { PageLoad } from "./$types";
export const load = (async ({ fetch, params }) => {
const reference = resourceHumanKey(params.workerRef);
const reference = resourceKey(params.workerRef);
const result = await loadJson<Worker>(
fetch,
workspaceApiPath(
@@ -16,9 +16,9 @@ export const load = (async ({ fetch, params }) => {
`/workers/${encodeURIComponent(reference)}`,
),
);
if (result.data?.human_key) {
if (result.data?.resource_key) {
const canonical = canonicalResourceReference(
result.data.human_key,
result.data.resource_key,
result.data.display_name,
);
if (params.workerRef !== canonical) {