Merge current main (fast-tests quality-backfill projects) into plugin install fixes

This commit is contained in:
gsxdsm
2026-06-04 22:42:06 -07:00
149 changed files with 23992 additions and 1368 deletions

View File

@@ -16,9 +16,10 @@ These tools are **not** part of the user-invokable extension surface. They are i
| `fn_task_document_write` | triage, executor, heartbeat | Save/update a named task document revision | `key` (string), `content` (string), `author?` (string) |
| `fn_task_document_read` | triage, executor, heartbeat | Read one task document or list all | `key?` (string) |
| `fn_workflow_list` | executor | List the project's custom workflows (read-only built-ins plus user definitions) | none |
| `fn_workflow_get` | executor | Fetch one workflow definition by id — name, description, builtin flag, and the full IR (nodes/edges/columns/artifacts/fields) as JSON | `workflow_id` (string) |
| `fn_workflow_select` | executor | Assign a custom workflow to a task (defaults to the current task) | `workflow_id` (string), `task_id?` (string) |
| `fn_workflow_create` | executor | Create a custom workflow definition from a graph IR (validated server-side) | `name` (string), `description?` (string), `ir` (object), `layout?` (object) |
| `fn_workflow_update` | executor | Update a custom workflow definition's name/description/ir/layout (built-ins cannot be edited) | `workflow_id` (string), `name?` (string), `description?` (string), `ir?` (object), `layout?` (object), `rehome_to?` (string) |
| `fn_workflow_create` | executor | Create a custom workflow definition from a graph IR (validated server-side). v2 IR supports step-inversion constructs: `parse-steps`, `foreach` (mode/isolation/concurrency/maxReworkCycles), `step-execute`, `step-review`, `code` nodes, `rework` edges, plus `artifacts` and custom `fields` declarations | `name` (string), `description?` (string), `ir` (object), `layout?` (object) |
| `fn_workflow_update` | executor | Update a custom workflow definition's name/description/ir/layout (built-ins cannot be edited; same step-inversion IR constructs as create; editing `fields` orphans rather than destroys existing task values) | `workflow_id` (string), `name?` (string), `description?` (string), `ir?` (object), `layout?` (object), `rehome_to?` (string) |
| `fn_workflow_delete` | executor | Delete a custom workflow definition (built-ins cannot be deleted); selecting tasks are re-homed to the default workflow's entry column | `workflow_id` (string) |
| `fn_task_promote` | executor | Promote a held task out of a manual-release hold column (defaults to the current task) | `task_id?` (string) |
| `fn_trait_list` | executor | List the registered column trait catalog (built-in and plugin traits) | none |
@@ -58,7 +59,7 @@ Note: step-session execution (`step-session-executor.ts`) reuses executor coordi
| Tool | Purpose | Parameters |
|---|---|---|
| `fn_task_update` | Update a spec step status (`pending`/`in-progress`/`done`/`skipped`) | `step` (number), `status` (enum) |
| `fn_task_update` | Update a spec step status (`pending`/`in-progress`/`done`/`skipped`), task dependencies, and/or workflow-defined custom field values | `step?` (number, 1-indexed), `status?` (enum), `dependencies?` (string[]), `custom_fields?` (object keyed by field id; validated against the workflow field schema, `null` clears a field) |
| `fn_task_add_dep` | Add a dependency to current task (confirmation-gated) | `task_id` (string), `confirm?` (boolean) |
| `fn_task_done` | Mark task complete and optionally store summary | `summary?` (string) |
| `fn_review_step` | Spawn step plan/code reviewer | `step` (number), `type` (`plan` \| `code`), `step_name` (string), `baseline?` (string) |

View File

@@ -276,12 +276,18 @@ describe("Workspace bootstrap script contract", () => {
const defaultTest = dashboardPkg.scripts?.test;
const defaultAppQuality = dashboardPkg.scripts?.["test:quality:app"];
const defaultApiQuality = dashboardPkg.scripts?.["test:quality:api"];
const apiCurated = dashboardPkg.scripts?.["test:quality:api:curated"];
const deepTest = dashboardPkg.scripts?.["test:deep"];
expect(defaultTest).toBe("pnpm run test:quality:app && pnpm run test:quality:api");
expect(defaultAppQuality).toContain("test:quality:app:foundation-api");
expect(defaultAppQuality).toContain("test:quality:app:settings");
expect(hasProjectArg(defaultApiQuality, "dashboard-api-quality")).toBe(true);
// The api lane chains curated + backfill sub-lanes; the curated sub-lane
// carries the explicit quality project, and the backfill lane is the
// curated-gate completeness net (broad glob minus curated minus skip-list).
expect(defaultApiQuality).toContain("test:quality:api:curated");
expect(defaultApiQuality).toContain("test:quality:api:backfill");
expect(hasProjectArg(apiCurated, "dashboard-api-quality")).toBe(true);
expect(hasProjectArg(defaultTest, "dashboard-app")).toBe(false);
expect(hasProjectArg(defaultTest, "dashboard-api")).toBe(false);

View File

@@ -166,7 +166,10 @@ afterEach(() => {
vi.useRealTimers();
});
async function waitForFrameContains(lastFrame: () => string | undefined, text: string, timeoutMs = 3000) {
// 10s bound: ink schedules frames on timer ticks and has flaked past 3s under
// loaded CI shards while passing instantly in isolation. vi.waitFor polls, so
// a generous bound adds zero time to passing runs.
async function waitForFrameContains(lastFrame: () => string | undefined, text: string, timeoutMs = 10_000) {
await vi.waitFor(() => {
expect(lastFrame() ?? "").toContain(text);
}, { timeout: timeoutMs });

View File

@@ -1613,6 +1613,15 @@ function TaskDetailScreen({
</Box>
)}
{/* Card-placed custom fields (U13/KTD-14): read-only bracketed labels. */}
{detail.customFields && detail.customFields.length > 0 && (
<Box flexDirection="row" gap={1} flexWrap="wrap" flexShrink={0}>
{detail.customFields.map((f) => (
<Text key={f.label} color="magenta">[{f.label}: {f.value}]</Text>
))}
</Box>
)}
<Box height={1} flexShrink={0} />
{/* Steps section */}

View File

@@ -230,6 +230,10 @@ export interface TaskDetailData {
currentStepIndex?: number;
steps: TaskStep[];
recentLogs: TaskLogEntry[]; // last ~200 entries on initial load
/** Card-placed custom field values, pre-rendered as read-only bracketed
* labels for the task detail view (U13/KTD-14). Absent/empty when the
* workflow declares no card fields or none have values. */
customFields?: Array<{ label: string; value: string }>;
}
export type TaskEvent =

View File

@@ -19,6 +19,7 @@ import {
isWorkflowColumnsEnabled,
resolveColumnFlags,
BUILTIN_CODING_WORKFLOW_IR,
parseWorkflowIr,
type WorkflowIrColumn,
type TraitFlags,
} from "@fusion/core";
@@ -2742,6 +2743,48 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
text: entry.outcome ? `${entry.action} → ${entry.outcome}` : entry.action,
source: entry.runContext?.agentId ? "agent" : "executor",
}));
// Card-placed custom fields → read-only bracketed labels
// (U13/KTD-14). Resolve the task's workflow IR, filter
// card-placed field defs, and render any present values.
// Best-effort: any resolution failure simply omits the chips.
let customFields: Array<{ label: string; value: string }> | undefined;
try {
const values = (t as { customFields?: Record<string, unknown> }).customFields;
if (values && Object.keys(values).length > 0) {
const selection = projectStore.getTaskWorkflowSelection(t.id);
const def = selection?.workflowId
? await projectStore.getWorkflowDefinition(selection.workflowId)
: undefined;
const ir = def
? (typeof def.ir === "string" ? parseWorkflowIr(def.ir) : def.ir)
: BUILTIN_CODING_WORKFLOW_IR;
const fields = ir.version === "v2" ? (ir.fields ?? []) : [];
const chips: Array<{ label: string; value: string }> = [];
for (const field of fields) {
if (field.render?.placement !== "card") continue;
const raw = values[field.id];
if (raw === undefined || raw === null || raw === "") continue;
const optLabel = (v: string): string =>
field.options?.find((o) => o.value === v)?.label ?? v;
let display: string;
if (field.type === "boolean") {
if (raw !== true) continue;
display = field.name;
} else if (field.type === "multi-enum" && Array.isArray(raw)) {
if (raw.length === 0) continue;
display = raw.map((v) => optLabel(String(v))).join(", ");
} else if (field.type === "enum") {
display = optLabel(String(raw));
} else {
display = String(raw);
}
chips.push({ label: field.name, value: display });
}
if (chips.length > 0) customFields = chips;
}
} catch {
customFields = undefined;
}
return {
id: t.id,
title: t.title,
@@ -2753,6 +2796,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
currentStepIndex: t.currentStep,
steps,
recentLogs,
...(customFields ? { customFields } : {}),
};
} catch {
// Task not found (deleted/archived between selection and fetch).