From 50c6cd677ea970f13c47316bf37b8f17e654c08e Mon Sep 17 00:00:00 2001 From: Hideaki Terai Date: Sun, 12 Apr 2026 13:50:28 +0900 Subject: [PATCH 1/3] fix(cli): always link events to a task in start/close Previously, running `ingest start` without --task left all subsequent events with task_id=NULL, making them invisible in `ingest resume`. Similarly, `ingest close` without --task only created an event without actually closing any task. - start: always create/find a task; title defaults to --summary or a timestamp-based "Session YYYY-MM-DD HH:MM" if both are omitted - close: when --task is omitted, close the most recently active task (same selection logic as `ingest resume`) Co-Authored-By: Claude Sonnet 4.6 --- src/cli/commands/close.ts | 64 ++++++++++++++++++--------------------- src/cli/commands/start.ts | 22 +++++++++----- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/cli/commands/close.ts b/src/cli/commands/close.ts index f7619d3..86415af 100644 --- a/src/cli/commands/close.ts +++ b/src/cli/commands/close.ts @@ -1,54 +1,50 @@ import { Command } from 'commander'; import { createEvent } from '../../core/eventService.js'; -import { updateTaskStatus, getTask } from '../../core/taskService.js'; +import { updateTaskStatus, getTask, getActiveTasks } from '../../core/taskService.js'; export function registerClose(program: Command): void { program .command('close') - .description('Close a task') + .description('Close a task (closes the most recently active task if --task is omitted)') .option('--task ', 'Task ID to close') .option('--summary ', 'Closing summary') .action(async (options) => { try { - let taskId = options.task; + let taskId: string | undefined = options.task; - // If no task specified, we still create the event - const summary = options.summary ?? (taskId ? `Closed task` : 'Closed work session'); - - if (taskId) { - const task = getTask(taskId); - if (!task) { - console.error(`Task not found: ${taskId}`); + // If no task ID specified, fall back to the most recently active task + if (!taskId) { + const activeTasks = getActiveTasks(); + if (activeTasks.length === 0) { + console.error('No active tasks found. Nothing to close.'); process.exit(1); } + taskId = activeTasks[0].id; + } - const event = createEvent({ - event_type: 'task_closed', - task_id: taskId, - project_id: task.project_id ?? undefined, - actor: 'human', - origin: 'manual', - summary, - }); + const task = getTask(taskId); + if (!task) { + console.error(`Task not found: ${taskId}`); + process.exit(1); + } - updateTaskStatus(taskId, 'closed'); + const summary = options.summary ?? `Closed task: ${task.title}`; - console.log(`Task closed: ${event.id}`); - console.log(`Task: ${task.title}`); - console.log(`Summary: ${event.summary}`); - console.log(`Time: ${event.occurred_at}`); - } else { - const event = createEvent({ - event_type: 'task_closed', - actor: 'human', - origin: 'manual', - summary, - }); + const event = createEvent({ + event_type: 'task_closed', + task_id: taskId, + project_id: task.project_id ?? undefined, + actor: 'human', + origin: 'manual', + summary, + }); - console.log(`Session closed: ${event.id}`); - console.log(`Summary: ${event.summary}`); - console.log(`Time: ${event.occurred_at}`); - } + updateTaskStatus(taskId, 'closed'); + + console.log(`Task closed: ${event.id}`); + console.log(`Task: ${task.title}`); + console.log(`Summary: ${event.summary}`); + console.log(`Time: ${event.occurred_at}`); } catch (err) { console.error('Error:', err instanceof Error ? err.message : String(err)); process.exit(1); diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index 2fde8ee..a7fad73 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -4,12 +4,19 @@ import { findOrCreateProject } from '../../core/projectService.js'; import { findOrCreateTopic } from '../../core/topicService.js'; import { createEvent } from '../../core/eventService.js'; +function defaultTaskTitle(summary?: string): string { + if (summary) return summary; + const now = new Date(); + const pad = (n: number) => String(n).padStart(2, '0'); + return `Session ${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`; +} + export function registerStart(program: Command): void { program .command('start') .description('Start a new task session') .option('--project ', 'Project name') - .option('--task ', 'Task title') + .option('--task <title>', 'Task title (auto-generated from --summary or timestamp if omitted)') .option('--topic <name>', 'Topic name') .option('--summary <text>', 'Summary') .action(async (options) => { @@ -21,12 +28,11 @@ export function registerStart(program: Command): void { console.log(`Project: ${project.name} (${project.id})`); } - let taskId: string | undefined; - if (options.task) { - const task = findOrCreateTask(options.task, projectId); - taskId = task.id; - console.log(`Task: ${task.title} (${task.id})`); - } + // Always create/find a task so that subsequent events are linked + const taskTitle = options.task ?? defaultTaskTitle(options.summary); + const task = findOrCreateTask(taskTitle, projectId); + const taskId = task.id; + console.log(`Task: ${task.title} (${task.id})`); let topicId: string | undefined; if (options.topic) { @@ -35,7 +41,7 @@ export function registerStart(program: Command): void { console.log(`Topic: ${topic.name} (${topic.id})`); } - const summary = options.summary ?? (options.task ? `Started task: ${options.task}` : 'Started work session'); + const summary = options.summary ?? `Started task: ${taskTitle}`; const event = createEvent({ event_type: 'task_started', From e4e43f60e80eeec6ee5e8ae427a98af26bf29633 Mon Sep 17 00:00:00 2001 From: Hideaki Terai <hideack@pepabo.com> Date: Sun, 12 Apr 2026 13:53:09 +0900 Subject: [PATCH 2/3] fix(close): require --task when multiple active tasks exist When only one active task exists, close it automatically as before. When multiple active tasks are running in parallel, print the full list and highlight the most recently started task as the suggested default, then exit with an error so the user must pass --task <id> explicitly. Adds getLastStartedActiveTask() to taskService to find the task whose most recent task_started event is the latest. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/cli/commands/close.ts | 22 ++++++++++++++++++---- src/core/taskService.ts | 12 ++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/close.ts b/src/cli/commands/close.ts index 86415af..accae9a 100644 --- a/src/cli/commands/close.ts +++ b/src/cli/commands/close.ts @@ -1,25 +1,39 @@ import { Command } from 'commander'; import { createEvent } from '../../core/eventService.js'; -import { updateTaskStatus, getTask, getActiveTasks } from '../../core/taskService.js'; +import { updateTaskStatus, getTask, getActiveTasks, getLastStartedActiveTask } from '../../core/taskService.js'; export function registerClose(program: Command): void { program .command('close') - .description('Close a task (closes the most recently active task if --task is omitted)') + .description('Close a task (auto-selects when only one active task exists)') .option('--task <id>', 'Task ID to close') .option('--summary <text>', 'Closing summary') .action(async (options) => { try { let taskId: string | undefined = options.task; - // If no task ID specified, fall back to the most recently active task if (!taskId) { const activeTasks = getActiveTasks(); if (activeTasks.length === 0) { console.error('No active tasks found. Nothing to close.'); process.exit(1); } - taskId = activeTasks[0].id; + if (activeTasks.length === 1) { + taskId = activeTasks[0].id; + } else { + // Multiple active tasks — show list with default suggestion + const suggested = getLastStartedActiveTask(); + console.error(`Multiple active tasks found. Specify --task <id>:`); + for (const t of activeTasks) { + const marker = suggested && t.id === suggested.id ? ' *' : ''; + console.error(` ${t.id} ${t.title}${marker}`); + } + if (suggested) { + console.error(`\n* Most recently started (suggested default)`); + console.error(` Run: ingest close --task ${suggested.id}`); + } + process.exit(1); + } } const task = getTask(taskId); diff --git a/src/core/taskService.ts b/src/core/taskService.ts index ff9a0d2..5571612 100644 --- a/src/core/taskService.ts +++ b/src/core/taskService.ts @@ -57,6 +57,18 @@ export function getActiveTasks(): Task[] { ).all() as Task[]; } +export function getLastStartedActiveTask(): Task | null { + const db = getDb(); + const row = db.prepare(` + SELECT t.* FROM tasks t + INNER JOIN events e ON e.task_id = t.id + WHERE t.status = 'active' AND e.event_type = 'task_started' + ORDER BY e.occurred_at DESC + LIMIT 1 + `).get() as Task | undefined; + return row ?? null; +} + export function getAllTasksWithMetrics( status?: TaskStatus ): Array<{ task: Task; metrics: TaskMetrics | null }> { From d1d384a065ec2b24dd6e69ddc877b7c9abbd73b2 Mon Sep 17 00:00:00 2001 From: Hideaki Terai <hideack@pepabo.com> Date: Sun, 12 Apr 2026 13:54:36 +0900 Subject: [PATCH 3/3] fix(close): accept task title as well as ID for --task option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Falls back to title lookup when the given value does not match any task ID, so both of these work: ingest close --task <id> ingest close --task "タスク名" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/cli/commands/close.ts | 5 +++-- src/core/taskService.ts | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/close.ts b/src/cli/commands/close.ts index accae9a..f0ec4c6 100644 --- a/src/cli/commands/close.ts +++ b/src/cli/commands/close.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { createEvent } from '../../core/eventService.js'; -import { updateTaskStatus, getTask, getActiveTasks, getLastStartedActiveTask } from '../../core/taskService.js'; +import { updateTaskStatus, getTask, findTaskByTitle, getActiveTasks, getLastStartedActiveTask } from '../../core/taskService.js'; export function registerClose(program: Command): void { program @@ -36,11 +36,12 @@ export function registerClose(program: Command): void { } } - const task = getTask(taskId); + const task = getTask(taskId) ?? findTaskByTitle(taskId); if (!task) { console.error(`Task not found: ${taskId}`); process.exit(1); } + taskId = task.id; const summary = options.summary ?? `Closed task: ${task.title}`; diff --git a/src/core/taskService.ts b/src/core/taskService.ts index 5571612..d3ab5e3 100644 --- a/src/core/taskService.ts +++ b/src/core/taskService.ts @@ -57,6 +57,11 @@ export function getActiveTasks(): Task[] { ).all() as Task[]; } +export function findTaskByTitle(title: string): Task | null { + const db = getDb(); + return (db.prepare('SELECT * FROM tasks WHERE title = ? LIMIT 1').get(title) as Task) ?? null; +} + export function getLastStartedActiveTask(): Task | null { const db = getDb(); const row = db.prepare(`