Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { z } from "zod";

import {
parseStatusPayload,
todoItemSchema,
todoIndexItemSchema,
periodicReportIndexItemSchema,
periodicReportIndexResponseSchema,
} from "../src/data/status";
Expand Down Expand Up @@ -76,6 +78,38 @@ function goal(id: string, activation: "active" | "stopped") {
};
}

// Native Todo identity is the stable todo_id; a Markdown source index is only
// a legacy display coordinate. Both forms must survive a full status parse.
const nativeTodo = { done: false, text: "Review public evidence", todo_id: "todo_native" };
const nativeStatus = basePayload({
attention_queue: {
available: true, item_count: 1, needs_user_or_controller: 0,
needs_controller: 0, needs_codex: 1, watching_external_evidence: 0,
items: [{ goal_id: "native", status: "running", waiting_on: "codex",
severity: "normal", recommended_action: "continue",
agent_todos: { items: [nativeTodo] },
project_asset: { owner: "agent", gate: "none", next_action: "review",
stop_condition: "accepted", agent_todos: {
items: [{ ...nativeTodo, index: null }],
recent_completed_advancement_items: [{ ...nativeTodo, todo_id: "todo_done", done: true }],
} },
}],
},
todo_index: { items: [{ ...nativeTodo, index: null, goal_id: "native" }] },
});
equal(nativeStatus.attention_queue.items[0].agent_todos?.items[0].index, undefined,
"native Todo without a source index remains addressable");
equal(nativeStatus.todo_index?.items[0].index, null,
"Todo index readback preserves the explicitly absent source coordinate");
assert(todoItemSchema.safeParse({ index: 3, done: false, text: "Legacy Todo" }).success,
"legacy source-index Todo remains valid without a stable id");
assert(!todoItemSchema.safeParse({ done: false, text: "Anonymous Todo" }).success,
"missing both source index and stable id is still invalid");
assert(!todoIndexItemSchema.safeParse({ goal_id: "native", index: null, done: false, text: "Anonymous Todo" }).success,
"Todo index must retain the same identity guard");
assert(!todoItemSchema.safeParse({ ...nativeTodo, index: "3" }).success,
"non-numeric source coordinates remain invalid");

const activePayload = basePayload({
goal_projection: {
schema_version: "loopx_goal_projection_scope_v0",
Expand Down
11 changes: 8 additions & 3 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export const reviewMaterialSchema = z.object({
});

export const todoItemSchema = z.object({
index: z.number(),
// Legacy Markdown Todos have a source index. Native Todos are addressed by
// todo_id and intentionally have no synthetic index.
index: z.number().optional().nullable(),
done: z.boolean(),
text: z.string(),
schema_version: z.string().optional().nullable(),
Expand Down Expand Up @@ -96,7 +98,10 @@ export const todoItemSchema = z.object({
revised_at: z.string(),
}).passthrough()).optional().default([]),
review_materials: z.array(reviewMaterialSchema).optional().default([]),
}).passthrough();
}).passthrough().refine(
(todo) => todo.index != null || Boolean(todo.todo_id?.trim()),
{ path: ["todo_id"], message: "Todo without a source index requires todo_id" },
);

export const todoGroupSchema = z.object({
source_section: z.string().optional().nullable(),
Expand All @@ -108,7 +113,7 @@ export const todoGroupSchema = z.object({
deferred_items: z.array(todoItemSchema).optional(),
});

export const todoIndexItemSchema = todoItemSchema.extend({
export const todoIndexItemSchema = todoItemSchema.safeExtend({
goal_id: z.string(),
source: z.string().optional().nullable(),
event_count: z.number().optional().default(0),
Expand Down
3 changes: 0 additions & 3 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ type PersonalAgentTodoItem = {
claimedBy?: string | null;
done: boolean;
evidence?: string | null;
index: number;
priority?: string | null;
status?: string | null;
taskClass?: string | null;
Expand Down Expand Up @@ -725,7 +724,6 @@ function personalAgentTodoFromItem(todo: TodoItem, row: GoalDirectoryRow): Perso
// Legacy summaries mark deferred entries checked; they are not completed work.
done: todo.status === "deferred" ? false : todo.done,
evidence: todo.evidence ? compactShareText(todo.evidence, 96) : null,
index: todo.index,
priority: todo.priority ?? null,
status: todo.status ?? null,
taskClass: todo.task_class ?? null,
Expand Down Expand Up @@ -787,7 +785,6 @@ function personalAgentTodoFromProjection(
return {
claimedBy: todo.claimed_by ?? null,
done: todo.status === "done" || todo.status === "completed",
index: -1,
priority: todo.priority ?? null,
status: todo.status ?? null,
taskClass: todo.task_class ?? null,
Expand Down
10 changes: 10 additions & 0 deletions examples/workspace-progressive-loading-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ function snapshot(id) {
const payload = structuredClone(require(resolve(root, "examples/status.example.json")));
payload.run_history.goals = [{ ...payload.run_history.goals[0], id, display_name: `${id} project`, activation_state: id === "archived" ? "stopped" : "active", registry_member: true }];
for (const item of payload.attention_queue.items) item.goal_id = id;
if (id === "ready") {
const native = { done: false, text: "Review public evidence", todo_id: "todo_native_ready" };
payload.attention_queue.items[0].agent_todos.items.unshift(native);
payload.attention_queue.items[0].project_asset = {
owner: "agent", gate: "none", next_action: "review", stop_condition: "accepted",
agent_todos: { items: [{ ...native, index: null }],
recent_completed_advancement_items: [{ ...native, todo_id: "todo_native_done", done: true }] },
};
payload.todo_index.items.unshift({ ...native, index: null, goal_id: id });
}
payload.workspace_registry_revision = directory.registry_revision;
return payload;
}
Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson.

| Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair |
| --- | --- | --- | --- | --- |
| `native_todo_status_index_schema_gap` | A Goal shows “status load failed / invalid response” after native Todos appear, while the scoped status endpoint returns valid JSON. | Exact scoped status response, Zod issue paths, Todo `todo_id` and `index` fields, native presentation contract, packaged Goal load. | The dashboard still requires every Todo to have a numeric source index, but native Todos deliberately use stable `todo_id` with absent or null index. One row rejects the entire Goal snapshot. | Accept nullable/absent index only when a nonempty stable Todo ID exists; keep numeric legacy indexes and reject anonymous or malformed rows. Render and act by Todo ID, then prove a mixed native/legacy scoped snapshot loads in the packaged UI. |
| `capability_catalog_editor_kind_drift` | Machine or Goal settings report an empty capability list even though the configuration API returns registered capabilities. | Live API catalog IDs and editor kinds, the dashboard's accepted field-kind schema, and the page's load-error state. | One new descriptor emits an unsupported field kind; strict validation rejects the shared catalog and the machine page presents the failed load as an empty registry. | Keep the published editor vocabulary aligned with the browser contract, check every built-in descriptor together, and show a retryable error when catalog loading or validation fails. Only a successfully loaded empty catalog may show the empty state. |
| `acceptance_scope_capture` | A bounded validation experiment leaves unrelated existing/new work unbound; a recorded blocker quiets replan without repairing admission. | Canonical contract scope/bindings, exact held generation, authorized configuration source, ordinary task validators and post-correction claim/lease readback. | Omitted scope silently imposed Goal-wide acceptance; repeated per-task binding masked the missing scope contract. | Require explicit scope on new owner configuration, preserve legacy persisted semantics/replay, and enforce one typed scope across admission, completion and verification freshness. Expose scope on existing read surfaces. Diagnose scope before proposing rebinding; a blocker ACK is neither a repair nor a handoff. Apply authorized corrections through CAS and validate independent work resumes while selected holds and ordinary validation remain. |
| `acceptance_hold_recovery_selection_split` | Newly created advancement work is acceptance-unbound, repeated vision replans never expose its hold, or a replan packet also selects an unrelated due monitor. | Canonical acceptance tasks, scoped source Todos, bounded trigger checkpoints, effective action and original Turn receipt. | Recovery covered stale associations only; generic vision gaps displaced hold identities; candidate inventory leaked into the selected execution target. | Route missing and stale associations through the existing bounded replan lane, retain exact hold checkpoints before generic gaps, and separate replan from candidate selection. Keep owner association and completion validation enforced. A new unbound repair Todo is not runnable recovery; only a qualified successor or concrete blocker settles the exact hold. Validate real File/SQLite CLI paths and receipt reentry without mutating an active Goal. |
Expand Down
Loading