Skip to content

Commit fdfd2ca

Browse files
byapparovclaude
andcommitted
refactor(session): remove the question tool and plan mode (#147)
The question tool and plan mode need a person at the keyboard. Headless runs only kept them in check through session deny rules, and the model was still offered their schemas on every step. - delete the question tool, the Question module and its rejection handling in the processor, and AICTRL_ENABLE_QUESTION_TOOL - delete the plan agent, plan_enter/plan_exit, the plan-mode reminders and plan-file helper, and AICTRL_EXPERIMENTAL_PLAN_MODE - delete two base prompts that nothing imports (copilot-gpt-5.txt, anthropic-20250930.txt) - drop the deny rules that run and the GitHub agent created for these tools - `aictrl run --agent plan` now fails with INVOCATION_INVALID_ARGUMENTS instead of falling back to the default agent, which can edit files Compatibility: the question/plan_enter/plan_exit permission keys still load as no-ops, and sessions whose history calls these tools still continue. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent c1bca11 commit fdfd2ca

33 files changed

Lines changed: 178 additions & 1501 deletions

‎.opencode/agent/translator.md‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -579,8 +579,6 @@ AICTRL_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS
579579
AICTRL_EXPERIMENTAL_EXA
580580
AICTRL_EXPERIMENTAL_OUTPUT_TOKEN_MAX
581581
AICTRL_EXPERIMENTAL_OXFMT
582-
AICTRL_EXPERIMENTAL_PLAN_MODE
583-
AICTRL_ENABLE_QUESTION_TOOL
584582
AICTRL_FAKE_VCS
585583
AICTRL_GIT_BASH_PATH
586584
AICTRL_MODEL
@@ -708,7 +706,6 @@ zenobi-us/aictrl-skillful
708706
.aictrl/commands/
709707
.aictrl/commands/test.md
710708
.aictrl/modes/
711-
.aictrl/plans/*.md
712709
.aictrl/plugins/
713710
.aictrl/skills/<name>/SKILL.md
714711
.aictrl/skills/git-release/SKILL.md

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ and [JSON event reference](EVENTS.md).
6868

6969
### Non-Interactive Execution
7070

71-
In headless mode, Aictrl automatically rejects all interactive permission requests (like `question` or `plan_enter`), ensuring your pipelines never hang.
71+
In headless mode, Aictrl automatically rejects all interactive permission requests, ensuring your pipelines never hang.
7272

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

‎packages/cli/src/acp/README.md‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,6 @@ aictrl acp
4444
aictrl acp --cwd /path/to/project
4545
```
4646

47-
### Question Tool Opt-In
48-
49-
ACP excludes `QuestionTool` by default.
50-
51-
```bash
52-
AICTRL_ENABLE_QUESTION_TOOL=1 aictrl acp
53-
```
54-
55-
Enable this only for ACP clients that support interactive question prompts.
56-
5747
### Programmatic
5848

5949
```typescript

‎packages/cli/src/agent/agent.ts‎

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import PROMPT_SUMMARY from "./prompt/summary.txt"
1515
import PROMPT_TITLE from "./prompt/title.txt"
1616
import { PermissionNext } from "@/permission/next"
1717
import { mergeDeep, pipe, sortBy, values } from "remeda"
18-
import { Global } from "@/global"
1918
import path from "path"
2019
import { Plugin } from "@/plugin"
2120
import { Skill } from "../skill"
@@ -60,9 +59,6 @@ export namespace Agent {
6059
"*": "ask",
6160
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
6261
},
63-
question: "deny",
64-
plan_enter: "deny",
65-
plan_exit: "deny",
6662
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
6763
read: {
6864
"*": "allow",
@@ -85,37 +81,7 @@ export namespace Agent {
8581
name: "build",
8682
description: "The default agent. Executes tools based on configured permissions.",
8783
options: {},
88-
permission: PermissionNext.merge(
89-
defaults,
90-
PermissionNext.fromConfig({
91-
question: "allow",
92-
plan_enter: "allow",
93-
}),
94-
user,
95-
),
96-
mode: "primary",
97-
native: true,
98-
},
99-
plan: {
100-
name: "plan",
101-
description: "Plan mode. Disallows all edit tools.",
102-
options: {},
103-
permission: PermissionNext.merge(
104-
defaults,
105-
PermissionNext.fromConfig({
106-
question: "allow",
107-
plan_exit: "allow",
108-
external_directory: {
109-
[path.join(Global.Path.data, "plans", "*")]: "allow",
110-
},
111-
edit: {
112-
"*": "deny",
113-
[path.join(".aictrl", "plans", "*.md")]: "allow",
114-
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
115-
},
116-
}),
117-
user,
118-
),
84+
permission: PermissionNext.merge(defaults, user),
11985
mode: "primary",
12086
native: true,
12187
},

‎packages/cli/src/cli/cmd/run.ts‎

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -495,23 +495,13 @@ export const RunCommand = cmd({
495495
await fail("--fork requires --continue or --session", "INVOCATION_INVALID_ARGUMENTS")
496496
}
497497

498-
const rules: PermissionNext.Ruleset = [
499-
{
500-
permission: "question",
501-
action: "deny",
502-
pattern: "*",
503-
},
504-
{
505-
permission: "plan_enter",
506-
action: "deny",
507-
pattern: "*",
508-
},
509-
{
510-
permission: "plan_exit",
511-
action: "deny",
512-
pattern: "*",
513-
},
514-
]
498+
// fail instead of the unknown-agent fallback: the default agent can edit files a plan run expects to stay untouched
499+
if (args.agent === "plan") {
500+
await fail(
501+
"The plan agent was removed. For a read-only run, deny edit and bash through `permission` config.",
502+
"INVOCATION_INVALID_ARGUMENTS",
503+
)
504+
}
515505

516506
function input(id?: string, body?: Record<string, unknown>) {
517507
if (!args.attach) return { ...(id ? { sessionID: id } : {}), ...body }
@@ -530,7 +520,7 @@ export const RunCommand = cmd({
530520

531521
const prompt = [...args.message, ...(args["--"] || [])].join(" ")
532522
const title = sessionTitle({ title: args.title, prompt, message, command: args.command })
533-
const result = await sdk.session.create(input(undefined, { title, permission: rules }))
523+
const result = await sdk.session.create(input(undefined, { title }))
534524
return result.data?.id
535525
}
536526

@@ -972,7 +962,7 @@ export const RunCommand = cmd({
972962
schemaVersion: SCHEMA_VERSION,
973963
model: args.model,
974964
agent: agent,
975-
permissions: PermissionNext.merge(agentInfo.permission, rules),
965+
permissions: agentInfo.permission,
976966
})
977967

978968
let aborted = false

‎packages/cli/src/config/config.ts‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,6 @@ export namespace Config {
630630
external_directory: PermissionRule.optional(),
631631
todowrite: PermissionAction.optional(),
632632
todoread: PermissionAction.optional(),
633-
question: PermissionAction.optional(),
634633
webfetch: PermissionAction.optional(),
635634
websearch: PermissionAction.optional(),
636635
codesearch: PermissionAction.optional(),
@@ -1043,15 +1042,13 @@ export namespace Config {
10431042
mode: z
10441043
.object({
10451044
build: Agent.optional(),
1046-
plan: Agent.optional(),
10471045
})
10481046
.catchall(Agent)
10491047
.optional()
10501048
.describe("@deprecated Use `agent` field instead."),
10511049
agent: z
10521050
.object({
10531051
// primary
1054-
plan: Agent.optional(),
10551052
build: Agent.optional(),
10561053
// subagent
10571054
general: Agent.optional(),

‎packages/cli/src/flag/flag.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export namespace Flag {
2020
export declare const AICTRL_DISABLE_PROJECT_CONFIG: boolean
2121
export const AICTRL_FAKE_VCS = process.env["AICTRL_FAKE_VCS"]
2222
export declare const AICTRL_CLIENT: string
23-
export const AICTRL_ENABLE_QUESTION_TOOL = truthy("AICTRL_ENABLE_QUESTION_TOOL")
2423
export declare const AICTRL_MODEL_STREAM_IDLE_TIMEOUT_MS: number
2524

2625
// Experimental
@@ -31,7 +30,6 @@ export namespace Flag {
3130
export const AICTRL_EXPERIMENTAL_OUTPUT_TOKEN_MAX = number("AICTRL_EXPERIMENTAL_OUTPUT_TOKEN_MAX")
3231
export const AICTRL_EXPERIMENTAL_OXFMT = AICTRL_EXPERIMENTAL || truthy("AICTRL_EXPERIMENTAL_OXFMT")
3332
export const AICTRL_DISABLE_FILETIME_CHECK = truthy("AICTRL_DISABLE_FILETIME_CHECK")
34-
export const AICTRL_EXPERIMENTAL_PLAN_MODE = AICTRL_EXPERIMENTAL || truthy("AICTRL_EXPERIMENTAL_PLAN_MODE")
3533
export const AICTRL_MODELS_URL = process.env["AICTRL_MODELS_URL"]
3634
export const AICTRL_MODELS_PATH = process.env["AICTRL_MODELS_PATH"]
3735

‎packages/cli/src/id/id.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export namespace Identifier {
66
session: "ses",
77
message: "msg",
88
permission: "per",
9-
question: "que",
109
user: "usr",
1110
part: "prt",
1211
pty: "pty",

‎packages/cli/src/question/index.ts‎

Lines changed: 0 additions & 171 deletions
This file was deleted.

‎packages/cli/src/session/index.ts‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { OutputSchema } from "./output-schema"
22
import { Slug } from "@aictrl/util/slug"
3-
import path from "path"
43
import { BusEvent } from "@/bus/bus-event"
54
import { Bus } from "@/bus"
65
import { Decimal } from "decimal.js"
@@ -24,7 +23,6 @@ import { Command } from "../command"
2423

2524
import type { Provider } from "@/provider/provider"
2625
import { PermissionNext } from "@/permission/next"
27-
import { Global } from "@/global"
2826
import type { LanguageModelV2Usage } from "@ai-sdk/provider"
2927
import { iife } from "@/util/iife"
3028

@@ -325,13 +323,6 @@ export namespace Session {
325323
return result
326324
}
327325

328-
export function plan(input: { slug: string; time: { created: number } }) {
329-
const base = Instance.project.vcs
330-
? path.join(Instance.worktree, ".aictrl", "plans")
331-
: path.join(Global.Path.data, "plans")
332-
return path.join(base, [input.time.created, input.slug].join("-") + ".md")
333-
}
334-
335326
export const get = fn(Identifier.schema("session"), async (id) => {
336327
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
337328
if (!row) throw new NotFoundError({ message: `Session not found: ${id}` })

0 commit comments

Comments
 (0)