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
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ and [JSON event reference](EVENTS.md).

In headless mode, Aictrl automatically rejects all interactive permission requests, ensuring your pipelines never hang.

`aictrl run` does not offer the `todowrite` tool: its definition adds about 2.4k tokens to
every request, and a one-shot run has nobody following a todo list. To track a plan, instruct
the agent to keep one in a file.

### Skill Scope

`aictrl run` does not list skills from `~/.claude`, `~/.agents` or `~/.opencode`;
Expand Down
4 changes: 0 additions & 4 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,6 @@ Provider failures and stream timeouts retain **1**; SIGINT/SIGTERM retain

In headless mode, Aictrl automatically rejects permissions that would otherwise prompt a user.

`aictrl run` does not offer the `todowrite` tool: its definition adds about 2.4k tokens to
every request, and a one-shot run has nobody following a todo list. To track a plan, instruct
the agent to keep one in a file.

### Skill Scope

`aictrl run` lists project skills (`.aictrl/skill(s)/`, and `.claude/skills/`,
Expand Down
9 changes: 1 addition & 8 deletions packages/cli/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,7 @@ export namespace Agent {
general: {
name: "general",
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
permission: PermissionNext.merge(
defaults,
PermissionNext.fromConfig({
todoread: "deny",
todowrite: "deny",
}),
user,
),
permission: PermissionNext.merge(defaults, user),
options: {},
mode: "subagent",
native: true,
Expand Down
14 changes: 1 addition & 13 deletions packages/cli/src/cli/cmd/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,7 @@ import type { Argv } from "yargs"

type AgentMode = "all" | "primary" | "subagent"

const AVAILABLE_TOOLS = [
"bash",
"read",
"write",
"edit",
"list",
"glob",
"grep",
"webfetch",
"task",
"todowrite",
"todoread",
]
const AVAILABLE_TOOLS = ["bash", "read", "write", "edit", "list", "glob", "grep", "webfetch", "task"]

const AgentCreateCommand = cmd({
command: "create",
Expand Down
12 changes: 0 additions & 12 deletions packages/cli/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import { TaskTool } from "../../tool/task"
import { SkillTool } from "../../tool/skill"
import { Skill } from "../../skill"
import { BashTool } from "../../tool/bash"
import { TodoWriteTool } from "../../tool/todo"
import { Locale } from "../../util/locale"
import { Log } from "../../util/log"
import { Shutdown } from "../shutdown"
Expand Down Expand Up @@ -286,16 +285,6 @@ function bash(info: ToolProps<typeof BashTool>) {
)
}

function todo(info: ToolProps<typeof TodoWriteTool>) {
block(
{
icon: "#",
title: "Todos",
},
info.input.todos.map((item) => `${item.status === "completed" ? "[x]" : "[ ]"} ${item.content}`).join("\n"),
)
}

function normalizePath(input?: string) {
if (!input) return ""
if (path.isAbsolute(input)) return path.relative(process.cwd(), input) || "."
Expand Down Expand Up @@ -566,7 +555,6 @@ export const RunCommand = cmd({
if (part.tool === "codesearch") return codesearch(props<typeof CodeSearchTool>(part))
if (part.tool === "websearch") return websearch(props<typeof WebSearchTool>(part))
if (part.tool === "task") return task(props<typeof TaskTool>(part))
if (part.tool === "todowrite") return todo(props<typeof TodoWriteTool>(part))
if (part.tool === "skill") return skill(props<typeof SkillTool>(part))
return fallback(part)
} catch {
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,8 +620,6 @@ export namespace Config {
bash: PermissionRule.optional(),
task: PermissionRule.optional(),
external_directory: PermissionRule.optional(),
todowrite: PermissionAction.optional(),
todoread: PermissionAction.optional(),
webfetch: PermissionAction.optional(),
websearch: PermissionAction.optional(),
codesearch: PermissionAction.optional(),
Expand Down
53 changes: 1 addition & 52 deletions packages/cli/src/session/prompt/anthropic.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,57 +20,8 @@ When the user directly asks about Aictrl (eg. "can Aictrl do...", "does Aictrl h
# Professional objectivity
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Aictrl honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.

# Task Management
You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.

It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.

Examples:

<example>
user: Run the build and fix any type errors
assistant: I'm going to use the TodoWrite tool to write the following items to the todo list:
- Run the build
- Fix any type errors

I'm now going to run the build using Bash.

Looks like I found 10 type errors. I'm going to use the TodoWrite tool to write 10 items to the todo list.

marking the first todo as in_progress

Let me start working on the first item...

The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
..
..
</example>
In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.

<example>
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the TodoWrite tool to plan this task.
Adding the following todos to the todo list:
1. Research existing metrics tracking in the codebase
2. Design the metrics collection system
3. Implement core metrics tracking functionality
4. Create export functionality for different formats

Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.

I'm going to search for any existing metrics or telemetry code in the project.

I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...

[Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]
</example>


# Doing tasks
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
-
- Use the TodoWrite tool to plan the task if required
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more.

- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.

Expand All @@ -93,8 +44,6 @@ user: What is the codebase structure?
assistant: [Uses the Task tool]
</example>

IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.

# Code References

When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.
Expand Down
20 changes: 1 addition & 19 deletions packages/cli/src/session/session.sql.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/project.sql"
import type { MessageV2 } from "./message-v2"
import type { PermissionNext } from "@/permission/next"
Expand Down Expand Up @@ -67,24 +67,6 @@ export const PartTable = sqliteTable(
(table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)],
)

export const TodoTable = sqliteTable(
"todo",
{
session_id: text()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
content: text().notNull(),
status: text().notNull(),
priority: text().notNull(),
position: integer().notNull(),
...Timestamps,
},
(table) => [
primaryKey({ columns: [table.session_id, table.position] }),
index("todo_session_idx").on(table.session_id),
],
)

export const PermissionTable = sqliteTable("permission", {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 TodoTable removed from schema with no DROP migration.

🤖 Fix with your agent
Fix this code review finding (aictrl-dev/cli PR #183, packages/cli/src/session/session.sql.ts:69):

Problem: TodoTable removed from schema with no DROP migration
Detail: TodoTable was deleted from the drizzle schema (and its re-export in src/storage/schema.ts) but no DROP TABLE migration was added: drizzle.config.ts drives drizzle-kit from "./src/**/*.sql.ts", so with packages/cli/migration/* still containing CREATE TABLE todo, `bun script/check-migrations.ts` (`drizzle-kit check`) deterministically exits 1 with "Schema has changes not captured in migrations!". The PR's own tests prove the table must keep existing (test/storage/json-migration.test.ts asserts `SELECT count(*) FROM todo` on a DB replayed from migration/*.sql; test/cli/run-continue-legacy-todo.test.ts raw-INSERTs into todo). Worse, the next `drizzle-kit generate` for any unrelated change will emit DROP TABLE todo, which breaks both legacy-todo tests on fresh databases and silently deletes legacy todo rows from existing user DBs. CI wiring of check-migrations could not be verified, but the gate script fails locally regardless and the poisoned-generate consequence holds either way.
Suggested fix: Keep the `todo` table declared in session.sql.ts (unused by app code, with a `// legacy (#182)` comment) so the drizzle schema matches migrations and legacy history stays queryable; only stop exporting/using it from src/storage/schema.ts. Alternatively, consciously accept the removal: add the DROP TABLE migration in this PR and rewrite the two tests that depend on the table existing — but that contradicts this PR's own legacy-history-replay test.

Implement the fix on the PR head branch and add a regression test that fails before the fix and passes after.
Why this matters

TodoTable was deleted from the drizzle schema (and its re-export in src/storage/schema.ts) but no DROP TABLE migration was added: drizzle.config.ts drives drizzle-kit from "./src/**/.sql.ts", so with packages/cli/migration/ still containing CREATE TABLE todo, bun script/check-migrations.ts (drizzle-kit check) deterministically exits 1 with "Schema has changes not captured in migrations!". The PR's own tests prove the table must keep existing (test/storage/json-migration.test.ts asserts SELECT count(*) FROM todo on a DB replayed from migration/*.sql; test/cli/run-continue-legacy-todo.test.ts raw-INSERTs into todo). Worse, the next drizzle-kit generate for any unrelated change will emit DROP TABLE todo, which breaks both legacy-todo tests on fresh databases and silently deletes legacy todo rows from existing user DBs. CI wiring of check-migrations could not be verified, but the gate script fails locally regardless and the poisoned-generate consequence holds either way.

  (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)],
)

export const PermissionTable = sqliteTable("permission", {

project_id: text()
.primaryKey()
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/session/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Ripgrep } from "../file/ripgrep"
import { Instance } from "../project/instance"

import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
import PROMPT_QWEN from "./prompt/qwen.txt"
import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"

Expand All @@ -23,7 +23,7 @@ export namespace SystemPrompt {
if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY]
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
return [PROMPT_QWEN]
}

export async function environment(model: Provider.Model) {
Expand Down
56 changes: 0 additions & 56 deletions packages/cli/src/session/todo.ts

This file was deleted.

51 changes: 2 additions & 49 deletions packages/cli/src/storage/json-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite"
import { Global } from "../global"
import { Log } from "../util/log"
import { ProjectTable } from "../project/project.sql"
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql"
import { SessionTable, MessageTable, PartTable, PermissionTable } from "../session/session.sql"
import path from "path"
import { existsSync } from "fs"
import { Filesystem } from "../util/filesystem"
Expand Down Expand Up @@ -32,7 +32,6 @@ export namespace JsonMigration {
sessions: 0,
messages: 0,
parts: 0,
todos: 0,
permissions: 0,
errors: [] as string[],
}
Expand All @@ -53,13 +52,11 @@ export namespace JsonMigration {
sessions: 0,
messages: 0,
parts: 0,
todos: 0,
permissions: 0,
errors: [] as string[],
}
const orphans = {
sessions: 0,
todos: 0,
permissions: 0,
}
const errs = stats.errors
Expand Down Expand Up @@ -103,12 +100,11 @@ export namespace JsonMigration {

// Pre-scan all files upfront to avoid repeated glob operations
log.info("scanning files...")
const [projectFiles, sessionFiles, messageFiles, partFiles, todoFiles, permFiles] = await Promise.all([
const [projectFiles, sessionFiles, messageFiles, partFiles, permFiles] = await Promise.all([
list("project/*.json"),
list("session/*/*.json"),
list("message/*/*.json"),
list("part/*/*.json"),
list("todo/*.json"),
list("permission/*.json"),
])

Expand All @@ -117,7 +113,6 @@ export namespace JsonMigration {
sessions: sessionFiles.length,
messages: messageFiles.length,
parts: partFiles.length,
todos: todoFiles.length,
permissions: permFiles.length,
})

Expand All @@ -127,7 +122,6 @@ export namespace JsonMigration {
sessionFiles.length +
messageFiles.length +
partFiles.length +
todoFiles.length +
permFiles.length,
)
const progress = options?.progress
Expand Down Expand Up @@ -299,46 +293,6 @@ export namespace JsonMigration {
}
log.info("migrated parts", { count: stats.parts })

// Migrate todos
const todoSessions = todoFiles.map((file) => path.basename(file, ".json"))
for (let i = 0; i < todoFiles.length; i += batchSize) {
const end = Math.min(i + batchSize, todoFiles.length)
const batch = await read(todoFiles, i, end)
const values = [] as any[]
for (let j = 0; j < batch.length; j++) {
const data = batch[j]
if (!data) continue
const sessionID = todoSessions[i + j]
if (!sessionIds.has(sessionID)) {
orphans.todos++
continue
}
if (!Array.isArray(data)) {
errs.push(`todo not an array: ${todoFiles[i + j]}`)
continue
}
for (let position = 0; position < data.length; position++) {
const todo = data[position]
if (!todo?.content || !todo?.status || !todo?.priority) continue
values.push({
session_id: sessionID,
content: todo.content,
status: todo.status,
priority: todo.priority,
position,
time_created: now,
time_updated: now,
})
}
}
stats.todos += insert(values, TodoTable, "todo")
step("todos", end - i)
}
log.info("migrated todos", { count: stats.todos })
if (orphans.todos > 0) {
log.warn("skipped orphaned todos", { count: orphans.todos })
}

// Migrate permissions
const permProjects = permFiles.map((file) => path.basename(file, ".json"))
const permValues = [] as any[]
Expand Down Expand Up @@ -381,7 +335,6 @@ export namespace JsonMigration {
sessions: stats.sessions,
messages: stats.messages,
parts: stats.parts,
todos: stats.todos,
permissions: stats.permissions,
errorCount: stats.errors.length,
duration: Math.round(performance.now() - start),
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/storage/schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { ControlAccountTable } from "../control/control.sql"
export { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql"
export { SessionTable, MessageTable, PartTable, PermissionTable } from "../session/session.sql"
export { ProjectTable } from "../project/project.sql"
Loading
Loading