diff --git a/README.md b/README.md index b10cd8d..9ccfe6c 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ pi -e ./src/index.ts | pi-tasks | pi-lgtm | |---|---| | Agent calls `TaskUpdate { status: "completed" }` | Blocked -- throws error | -| No evidence required | `lgtm_ask` requires evidence, 2 failure modes, evidence vs failures | +| No evidence required | `lgtm_ask` requires evidence, 2 failure modes, falsification test | | Tasks complete immediately | Agent sets `pending_approval`, human runs `/lgtm ` | | No done criterion | `done_criterion` required on create: falsifiable observation | @@ -47,7 +47,7 @@ Stripped: `TaskExecute`, `TaskOutput`, `TaskStop`, `process-tracker.ts`, subagen ### `TaskCreate` ``` -subject, description, done_criterion (required), activeForm (optional) +subject, description, done_criterion (required), progress_label (optional) ``` `done_criterion` must be a falsifiable observation: what you expect to see AND what you would see if it is wrong. Example: `"All 92 tests pass. If wrong: type errors in build or failures in task-store.test.ts."` @@ -72,10 +72,10 @@ The epistemic gate. Required fields: |---|---| | `taskId` | Task to submit | | `evidence` | Exact command run + output, commit hash, config/seeds, file paths. "I ran X and got Y" not "I wrote X". | -| `failure_mode_1` | Most likely way this is wrong despite evidence | -| `failure_mode_2` | Second most likely failure mode | -| `evidence_vs_failures` | How would evidence look different if FM1 or FM2 were true? | -| `evidence_files` | Optional file paths to inspect (validated: must exist) | +| `failure_likely` | Most likely way this is wrong despite evidence | +| `failure_sneaky` | Perverse/silent failure that looks like success superficially | +| `falsification_test` | What you ran and what you got, so both you and the human can sanity-check it. Why that result could not occur if a failure mode were real. | +| `verification_hints` | Where to look and what to check. Descriptions of evidence locations. | | `remaining_uncertainty` | What is NOT tested, deferred edge cases, known limitations | After calling this, the task shows `👀` and is only completable via `/lgtm `. Evidence is stored on the task so the human can review it hours later without scrolling back. diff --git a/src/index.ts b/src/index.ts index 46b9f65..bc852fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,7 @@ const REMINDER_INTERVAL = 4; const AUTO_CLEAR_DELAY = 4; const SYSTEM_REMINDER = ` -The task tools haven't been used recently. If working on tasks, use TaskCreate (requires done_criterion), TaskUpdate for status, and lgtm_ask when ready for human sign-off. Tasks can only be completed via /lgtm after calling lgtm_ask. Ignore if not applicable. Never mention this reminder to the user. +The LGTM sign-off task tools haven't been used recently. If working on tasks, use TaskCreate (requires done_criterion), TaskUpdate for status, and lgtm_ask when ready for human sign-off. Tasks can only be completed via /lgtm after calling lgtm_ask. These are sign-off tasks: agents propose evidence, humans approve. One task per piece of evidence or decision gate. Ignore if not applicable. Never mention this reminder to the user. `; export default function (pi: ExtensionAPI) { @@ -148,7 +148,7 @@ export default function (pi: ExtensionAPI) { pi.registerTool({ name: "TaskCreate", label: "TaskCreate", - description: `Create a task with a clear done_criterion. + description: `Create an LGTM sign-off task with a clear done_criterion. ## When to Use @@ -160,7 +160,7 @@ export default function (pi: ExtensionAPI) { - **subject**: Brief actionable title - **description**: Detailed description with context - **done_criterion**: REQUIRED. Falsifiable observation that distinguishes done from fail/null/incomplete/silent-fail. State expected AND wrong-case observations (e.g., "All 92 tests pass. If wrong: type errors in build or test failures in task-store.test.ts") -- **activeForm** (optional): Present continuous for spinner +- **progress_label** (optional): What the agent is currently doing, shown during in-progress tasks Tasks are completed only via /lgtm after calling lgtm_ask with evidence.`, promptGuidelines: [ @@ -172,13 +172,13 @@ Tasks are completed only via /lgtm after calling lgtm_ask with evidence.`, subject: Type.String({ description: "Brief task title" }), description: Type.String({ description: "Detailed description" }), done_criterion: Type.String({ description: "Falsifiable observation that distinguishes DONE from fail, null result, incomplete, or silent failure. State what you expect to see AND what you'd see if it's wrong." }), - activeForm: Type.Optional(Type.String({ description: "Present continuous for spinner" })), + progress_label: Type.Optional(Type.String({ description: "What the agent is currently doing, shown during in-progress tasks" })), metadata: Type.Optional(Type.Record(Type.String(), Type.Any())), }), execute(_toolCallId, params, _signal, _onUpdate, _ctx) { autoClear.resetBatchCountdown(); - const task = store.create(params.subject, params.description, params.done_criterion, params.activeForm, params.metadata); + const task = store.create(params.subject, params.description, params.done_criterion, params.progress_label, params.metadata); widget.update(); return Promise.resolve(textResult(`Task #${task.id} created: ${task.subject}\nDone criterion: ${task.done_criterion}`)); }, @@ -191,7 +191,7 @@ Tasks are completed only via /lgtm after calling lgtm_ask with evidence.`, pi.registerTool({ name: "TaskList", label: "TaskList", - description: `List all tasks. Tasks with 👀 are pending human sign-off via /lgtm.`, + description: `List all LGTM sign-off tasks. Tasks with 👀 are pending human sign-off via /lgtm.`, parameters: Type.Object({}), execute(_toolCallId, _params, _signal, _onUpdate, _ctx) { @@ -208,7 +208,6 @@ Tasks are completed only via /lgtm after calling lgtm_ask with evidence.`, const lines = sorted.map(task => { let line = `#${task.id} [${task.status}] ${task.subject}`; if (task.pending_approval && task.status !== "completed") line += " 👀"; - if (task.owner) line += ` (${task.owner})`; if (task.blockedBy.length > 0) { const openBlockers = task.blockedBy.filter(bid => { const blocker = store.get(bid); @@ -230,7 +229,7 @@ Tasks are completed only via /lgtm after calling lgtm_ask with evidence.`, pi.registerTool({ name: "TaskGet", label: "TaskGet", - description: `Get full task details including done_criterion and approval state.`, + description: `Get full LGTM sign-off task details including done_criterion and approval state.`, parameters: Type.Object({ taskId: Type.String({ description: "Task ID to retrieve" }), }), @@ -245,7 +244,6 @@ Tasks are completed only via /lgtm after calling lgtm_ask with evidence.`, `Status: ${task.status}${task.pending_approval && task.status !== "completed" ? " 👀 (pending sign-off)" : ""}`, `Done criterion: ${task.done_criterion}`, ]; - if (task.owner) lines.push(`Owner: ${task.owner}`); lines.push(`Description: ${desc}`); if (task.blockedBy.length > 0) { const openBlockers = task.blockedBy.filter(bid => { @@ -269,7 +267,7 @@ Tasks are completed only via /lgtm after calling lgtm_ask with evidence.`, pi.registerTool({ name: "TaskUpdate", label: "TaskUpdate", - description: `Update task fields or status. + description: `Update LGTM sign-off task fields or status. Status: pending -> in_progress -> (call lgtm_ask) -> /lgtm -> completed @@ -283,14 +281,13 @@ Cannot set status=completed here. Use lgtm_ask then /lgtm .`, ], description: "New status. Cannot set completed — use /lgtm after lgtm_ask.", })), - subject: Type.Optional(Type.String()), - description: Type.Optional(Type.String()), - done_criterion: Type.Optional(Type.String()), - activeForm: Type.Optional(Type.String()), - owner: Type.Optional(Type.String()), + subject: Type.Optional(Type.String({ description: "Brief task title" })), + description: Type.Optional(Type.String({ description: "Detailed description" })), + done_criterion: Type.Optional(Type.String({ description: "Falsifiable observation distinguishing done from fail" })), + progress_label: Type.Optional(Type.String({ description: "What the agent is currently doing" })), metadata: Type.Optional(Type.Record(Type.String(), Type.Any())), - addBlocks: Type.Optional(Type.Array(Type.String())), - addBlockedBy: Type.Optional(Type.Array(Type.String())), + add_blocks: Type.Optional(Type.Array(Type.String(), { description: "Task IDs this task blocks" })), + add_blocked_by: Type.Optional(Type.Array(Type.String(), { description: "Task IDs that block this task" })), }), execute(_toolCallId, params, _signal, _onUpdate, _ctx) { @@ -337,19 +334,19 @@ After this, task enters pending sign-off state — only completable via /lgtm `- ${f}`).join("\n")}` + const hintsSection = params.verification_hints?.length + ? `\n### Verification hints\n${params.verification_hints.map(h => `- ${h}`).join("\n")}` : ""; const uncertaintySection = params.remaining_uncertainty ? `\n### Remaining uncertainty\n${params.remaining_uncertainty}` @@ -388,10 +381,10 @@ After this, task enters pending sign-off state — only completable via /lgtm 0 ? evidenceParts.join("\n\n") : "(no stored evidence)"; diff --git a/src/task-store.ts b/src/task-store.ts index 7102432..15732ce 100644 --- a/src/task-store.ts +++ b/src/task-store.ts @@ -83,7 +83,7 @@ export class TaskStore { finally { releaseLock(this.lockPath); } } - create(subject: string, description: string, done_criterion: string, activeForm?: string, metadata?: Record): Task { + create(subject: string, description: string, done_criterion: string, progress_label?: string, metadata?: Record): Task { return this.withLock(() => { const now = Date.now(); const task: Task = { @@ -91,7 +91,7 @@ export class TaskStore { subject, description, done_criterion, pending_approval: false, status: "pending", - activeForm, owner: undefined, + progress_label, metadata: metadata ?? {}, blocks: [], blockedBy: [], createdAt: now, updatedAt: now, @@ -117,11 +117,10 @@ export class TaskStore { description?: string; done_criterion?: string; pending_approval?: boolean; - activeForm?: string; - owner?: string; + progress_label?: string; metadata?: Record; - addBlocks?: string[]; - addBlockedBy?: string[]; + add_blocks?: string[]; + add_blocked_by?: string[]; }): { task: Task | undefined; changedFields: string[]; warnings: string[] } { return this.withLock(() => { const task = this.tasks.get(id); @@ -148,8 +147,7 @@ export class TaskStore { if (fields.description !== undefined) { task.description = fields.description; changedFields.push("description"); } if (fields.done_criterion !== undefined) { task.done_criterion = fields.done_criterion; changedFields.push("done_criterion"); } if (fields.pending_approval !== undefined) { task.pending_approval = fields.pending_approval; changedFields.push("pending_approval"); } - if (fields.activeForm !== undefined) { task.activeForm = fields.activeForm; changedFields.push("activeForm"); } - if (fields.owner !== undefined) { task.owner = fields.owner; changedFields.push("owner"); } + if (fields.progress_label !== undefined) { task.progress_label = fields.progress_label; changedFields.push("progress_label"); } if (fields.metadata !== undefined) { for (const [key, value] of Object.entries(fields.metadata)) { @@ -159,8 +157,8 @@ export class TaskStore { changedFields.push("metadata"); } - if (fields.addBlocks?.length) { - for (const targetId of fields.addBlocks) { + if (fields.add_blocks?.length) { + for (const targetId of fields.add_blocks) { if (!task.blocks.includes(targetId)) task.blocks.push(targetId); const target = this.tasks.get(targetId); if (target && !target.blockedBy.includes(id)) { target.blockedBy.push(id); target.updatedAt = Date.now(); } @@ -171,8 +169,8 @@ export class TaskStore { changedFields.push("blocks"); } - if (fields.addBlockedBy?.length) { - for (const targetId of fields.addBlockedBy) { + if (fields.add_blocked_by?.length) { + for (const targetId of fields.add_blocked_by) { if (!task.blockedBy.includes(targetId)) task.blockedBy.push(targetId); const target = this.tasks.get(targetId); if (target && !target.blocks.includes(id)) { target.blocks.push(id); target.updatedAt = Date.now(); } diff --git a/src/types.ts b/src/types.ts index 492d969..a64c0e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,8 +11,7 @@ export interface Task { done_criterion: string; // required: what "done" looks like pending_approval: boolean; // set by lgtm_ask, required before /lgtm status: TaskStatus; - activeForm?: string; - owner?: string; + progress_label?: string; metadata: Record; blocks: string[]; blockedBy: string[]; diff --git a/src/ui/task-widget.ts b/src/ui/task-widget.ts index 4272af1..27cdc19 100644 --- a/src/ui/task-widget.ts +++ b/src/ui/task-widget.ts @@ -5,7 +5,7 @@ * ✔ completed tasks (strikethrough + dim) * ◼ in_progress tasks * ◻ pending tasks - * ✳/✽ actively executing task (star spinner with activeForm text) + * ✳/✽ actively executing task (star spinner with progress_label text) */ import { truncateToWidth } from "@mariozechner/pi-tui"; @@ -31,7 +31,7 @@ export type UICtx = { /** Star spinner frames for animated active task indicator (matches Claude Code). */ const SPINNER = ["✳", "✴", "✵", "✶", "✷", "✸", "✹", "✺", "✻", "✼", "✽"]; -const MAX_VISIBLE_TASKS = 10; +const MAX_VISIBLE_TASKS = 5; /** Per-task runtime metrics (elapsed time, token usage). */ export interface TaskMetrics { @@ -166,7 +166,7 @@ export class TaskWidget { let text: string; if (isActive) { - const form = task.activeForm || task.subject; + const form = task.progress_label || task.subject; const agentId = task.metadata?.agentId; const agentLabel = agentId ? ` (agent ${agentId.slice(0, 5)})` : ""; const m = this.metrics.get(task.id);