Skip to content

feat(config): add todosDirectory setting for project-local todo persistence#5928

Open
mvanhorn wants to merge 2 commits into
QwenLM:mainfrom
mvanhorn:feat/5836-todos-directory
Open

feat(config): add todosDirectory setting for project-local todo persistence#5928
mvanhorn wants to merge 2 commits into
QwenLM:mainfrom
mvanhorn:feat/5836-todos-directory

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

What this PR does

Adds a todosDirectory setting so todos created by the todo-write tool can be persisted inside the project (for example .qwen/todos) instead of only under the global runtime directory, letting task state be committed and synced via Git across machines and teammates. This mirrors the existing plansDirectory mechanism exactly, as proposed by maintainer @doudouOUC in #5836 ("add todosDirectory config param following the existing plansDirectory pattern"). When the setting is unset, todos continue to use the existing global runtime todos/ directory, so default behavior is unchanged. The new Storage.getTodosDir(projectRoot, todosDirectory) mirrors getPlansDir: it trims the value, resolves it against the project root, and asserts the result stays within the project root (throwing FatalConfigError otherwise). The value is threaded through Config (a todosDirectory param plus a getTodosDir() accessor) and the CLI config loader, and todoWrite now resolves its read/write directory from config.getTodosDir() instead of the hardcoded runtime path. The packaged VS Code settings JSON schema was regenerated to include the new key.

Why it's needed

Todos currently live under Storage.getRuntimeBaseDir()/todos, outside version control, so project task state is lost when switching machines and cannot be shared with teammates. The reporter asked for an option to persist todos inside the project so they can be synced via Git, matching what plans already supports. Opting into a project-local directory makes todos a first-class, syncable part of the repo, consistent with how plans are already handled.

Reviewer Test Plan

How to verify

This is a non–user-visible config addition. With todosDirectory unset, todos still write to the global runtime todos/ directory (no behavior change). Setting "todosDirectory": ".qwen/todos" causes the todo-write tool to read and write todo JSON within the project root; a relative path is resolved from the project root, and a path escaping the project root throws a FatalConfigError (matching the plansDirectory containment guard).

Commands run locally:

  • npx vitest run packages/core/src/config/storage.test.ts packages/core/src/tools/todoWrite.test.ts packages/cli/src/config/settingsSchema.test.ts — 126 tests pass.
  • npm run generate:settings-schema — regenerated and committed the packaged VS Code schema.
  • ESLint clean on the touched files.

Added test coverage: getTodosDir default fallback, configured relative-path resolution, out-of-root rejection, and the missing-projectRoot guard; the schema key's type/category/default; and todo read/write round-trips honoring the configured directory.

Evidence (Before & After)

N/A (non–user-visible config and persistence-path change; verified via unit tests above).

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Local unit tests via vitest; no runtime sandbox required.

Risk & Scope

  • Main risk or tradeoff: low; default (unset) behavior is byte-identical to today since getTodosDir() returns the same global runtime todos/ path when the setting is absent.
  • Not validated / out of scope: the exported readTodosForSession/listTodoSessions SDK helpers (no Config in scope, no in-repo callers) keep using the default directory; mirrors the plansDirectory constructor-caching approach.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #5836

…stence

Add a todosDirectory setting mirroring plansDirectory so todos written by
the todo-write tool can be persisted inside the project (e.g. .qwen/todos)
and synced via Git. When unset, todos continue to use the global runtime
todos directory, so default behavior is unchanged.

Closes QwenLM#5836
@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @mvanhorn!

Template looks good ✓ — all required sections present and filled in.

On direction: this is a clean fit. Issue #5836 explicitly requested todosDirectory following the plansDirectory pattern, and a maintainer (@doudouOUC) proposed it. The problem is real — todos living only in the global runtime directory means task state can't be committed or shared, which is a gap when plansDirectory already offers that for plans. No concerns here.

On approach: the scope is tight and proportional. +203/-16 across 10 files, every change directly serves the stated goal. The Storage.getTodosDir mirrors getPlansDir exactly, the Config threading is minimal, and the test coverage looks comprehensive (default fallback, relative-path resolution, escape rejection, missing-projectRoot guard, plus todoWrite integration). No drive-by refactors or scope creep. The decision to keep readTodosForSession/listTodoSessions on the default directory is reasonable and explicitly documented.

Moving on to code review. 🔍

中文说明

感谢贡献,@mvanhorn

模板完整 ✓ — 所有必填部分均已填写。

方向:完全契合。Issue #5836 明确要求按照 plansDirectory 模式添加 todosDirectory,且由维护者 @doudouOUC 提出。问题真实存在——todos 仅存于全局运行时目录,无法提交到 Git 或与团队共享,而 plansDirectory 已为 plans 提供了此能力。方向没有问题。

方案:范围精准。10 个文件 +203/-16 行,每处改动都服务于目标。Storage.getTodosDir 完全镜像 getPlansDir,Config 传参最小化,测试覆盖全面(默认回退、相对路径解析、路径逃逸拒绝、缺少 projectRoot 的守卫,以及 todoWrite 集成测试)。没有夹带无关重构或范围蔓延。readTodosForSession/listTodoSessions 保持默认目录的决定合理且已明确记录。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Clean implementation. Storage.getTodosDir() mirrors getPlansDir() exactly — same path resolution, containment guard (FatalConfigError), and fallback pattern. One minor elegance: the PR avoids adding a redundant assertTodosDirWithinTargetDir() on Config since Storage.getTodosDir() already enforces containment. The todoWrite refactoring is minimal — getTodoFilePath takes todoDir as a parameter instead of computing it from a hardcoded constant, and the mkdir logic in writeTodosToFile is cleaned up to not redundantly derive the directory from the file path.

No correctness bugs, security issues, or over-abstraction. Test coverage is proportional: 4 new getTodosDir tests (default, relative path, escape rejection, missing projectRoot), 1 new todoWrite integration test (configured directory round-trip), and 1 new schema test. All pass. The 4 pre-existing test failures (~/.qwen path expectations) are CI environment issues ($QWEN_HOME redirects to a custom path) — not caused by this PR.

Unit test results (PR code):

  • settingsSchema.test.ts: 30/30 ✅
  • storage.test.ts: 64/67 (3 pre-existing env failures, all 4 new getTodosDir tests pass)
  • todoWrite.test.ts: 28/29 (1 pre-existing env failure, new configured-directory test passes)

Real-Scenario Testing

The todo_write tool isn't available to the LLM in this CI environment (no model API key configured for the tool), so full end-to-end CLI testing wasn't possible. Instead, I verified the PR's core functionality via direct integration testing against the built PR artifacts.

Before (installed qwen)

=== BEFORE (installed qwen) ===
getTodosDir count across all chunks: 0 (does not exist)
getPlansDir present:
  chunk-C4K3FEQ2.js: 2
  chunk-2X2WXY6L.js: 3

After (this PR)

=== AFTER (PR build) ===
getTodosDir: 1 (new method in storage.js)
getPlansDir: 2 (coexists)

=== INTEGRATION TEST ===
getTodosDir(): /home/github-runner/actions-runner-15/_work/_temp/qwen-home/todos
Configured: /home/github-runner/.../test-project/.qwen/todos
Escape rejected: todosDirectory must resolve within the project root.

All three behaviors verified:

  1. Default fallback → runtime todos/ directory (same as today, no behavior change)
  2. Configured relative path → resolves against project root to .qwen/todos
  3. Escape rejectionFatalConfigError when path tries to leave project root
中文说明

代码审查

实现干净。Storage.getTodosDir() 完全镜像 getPlansDir()——相同的路径解析、包含守卫(FatalConfigError)和回退模式。一个小优点:PR 没有在 Config 上添加冗余的 assertTodosDirWithinTargetDir(),因为 Storage.getTodosDir() 已经强制执行了包含检查。todoWrite 的重构最小化——getTodoFilePath 改为接收 todoDir 参数而非从硬编码常量计算,writeTodosToFile 中的 mkdir 逻辑也清理为不再从文件路径冗余地推导目录。

无正确性 bug、安全问题或过度抽象。测试覆盖合理:4 个新的 getTodosDir 测试(默认、相对路径、路径逃逸拒绝、缺少 projectRoot),1 个新的 todoWrite 集成测试(配置目录往返),1 个新的 schema 测试。全部通过。4 个预存测试失败(~/.qwen 路径预期)是 CI 环境问题($QWEN_HOME 重定向到自定义路径),与 PR 无关。

真实场景测试

CI 环境中 LLM 没有 todo_write 工具(未配置模型 API key),无法进行完整的端到端 CLI 测试。改为对构建好的 PR 产物进行直接集成测试。

已验证的三种行为:

  1. 默认回退 → 运行时 todos/ 目录(与当前一致,无行为变化)
  2. 配置相对路径 → 相对于项目根目录解析为 .qwen/todos
  3. 路径逃逸拒绝 → 当路径试图离开项目根目录时抛出 FatalConfigError

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This PR does exactly what it should, and nothing it shouldn't. The implementation is a clean mirror of the existing plansDirectory pattern — same Storage.getTodosDir() structure, same containment guard, same Config threading. The diff is proportional: +203/-16 across 10 files, every change serves the stated goal, no drive-by refactors or scope creep.

My independent proposal from the code review stage matched the PR's approach closely. One small thing I noted: the PR is actually slightly more elegant than plansDirectory — it avoids a redundant assertTodosDirWithinTargetDir() on Config since the validation already happens inside Storage.getTodosDir(). Not a concern, just a sign the author understood the pattern rather than blindly copying it.

Integration testing confirms the three key behaviors: default fallback to the runtime directory (no behavior change when unset), relative path resolution against the project root, and FatalConfigError on escape attempts. Unit tests are solid with good coverage of edge cases.

The one testing limitation is that full end-to-end CLI testing (actually triggering todo_write through the LLM) wasn't possible in this CI environment — no model API key configured for the tool. But the config path itself is verified, and the unit tests cover the integration between todoWrite and config.getTodosDir().

LGTM, approving. ✅

中文说明

这个 PR 做了该做的事,没有多余的东西。实现干净地镜像了现有的 plansDirectory 模式——相同的 Storage.getTodosDir() 结构、相同的包含守卫、相同的 Config 传参。diff 比例合理:10 个文件 +203/-16,每处改动都服务于目标,无夹带重构或范围蔓延。

我在代码审查阶段的独立方案与 PR 的方法高度吻合。一个细节值得注意:PR 实际上比 plansDirectory 更优雅——避免了 Config 上冗余的 assertTodosDirWithinTargetDir(),因为验证已在 Storage.getTodosDir() 内完成。这说明作者理解了模式而非盲目复制。

集成测试确认了三个关键行为:默认回退到运行时目录(未设置时无行为变化)、相对路径基于项目根目录解析、逃逸尝试时抛出 FatalConfigError。单元测试覆盖扎实。

唯一的测试限制是 CI 环境中无法进行完整的端到端 CLI 测试(未配置 todo_write 工具的模型 API key),但配置路径本身已验证,单元测试覆盖了 todoWriteconfig.getTodosDir() 的集成。

LGTM,批准。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅


// 4. Write new todos AFTER all validation passes
await writeTodosToFile(finalTodos, sessionId);
await writeTodosToFile(todoDir, finalTodos, sessionId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The configured todosDirectory is only realpath-contained when Config is constructed, but this write path later reuses the cached directory without revalidating it or the final file path. If .qwen/todos is replaced by a symlink after startup, fs.mkdir / atomicWriteFile will follow it and persist the todo JSON outside targetDir, which breaks the new project-root containment guarantee. The plan storage path handles the same risk by re-checking before I/O and checking the final file after write. Please mirror that for todos: track whether todosDirectory was configured, build todo file paths through a helper that can assert containment, re-run Storage.assertPathWithinDirectory before read/existence/write paths, and verify or roll back after writes.

— GPT-5 via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-affirming Request changes: the open [Critical] containment concern at todoWrite.ts:422 (configured todosDirectory validated only at construction, not re-checked or rolled back before I/O) is the merge blocker and is still unaddressed.

The PR describes itself as mirroring plansDirectory "exactly," but several safety/UX mechanisms plansDirectory carries were not brought across. Three are inline above (session-id sanitization, the legacy-location migration warning, the docs .gitignore note). Three more that don't anchor to a single changed line:

  • SDK read/write split (latent)readTodosForSession / listTodoSessions (todoWrite.ts:537,545) call Storage.getTodosDir() with no args, so they always read the global runtime dir even when a project sets todosDirectory. You correctly scoped this out in Risk & Scope and there are no in-repo callers today, but the next caller wiring these into session-recovery/webui gets silently-empty results. A one-line code comment at the call sites would make the landmine visible in-file, not just in the PR description.
  • Test parityplansDirectory has a loadCliConfig plansDirectory test in packages/cli/src/config/config.test.ts and three symlink-escape tests for getPlansDir in storage.test.ts. todosDirectory has neither: no CLI-loader/Config-level test, and the getTodosDir suite stubs realpath to the real fs, so only the lexical ../ escape is exercised — the symlink-containment limb (the one relevant to the :422 concern) is untested.
  • Does this actually deliver #5836? Todo files are named ${sessionId}.json where sessionId is a fresh per-process UUID. Committing .qwen/todos to git therefore does not give the cross-device "resume my task list" outcome the issue asks for: machine B starts a new session UUID and never reads the committed file. The storage-location half ships; the retrieval half (a stable, project-scoped key) does not. Worth confirming whether that's in scope or a follow-up — otherwise Fixes #5836 overstates what lands.

Minor (non-blocking): the default is documented as ~/.qwen/todos but resolves to getRuntimeBaseDir()/todos, which differs when QWEN_RUNTIME_DIR is set; and the new todosDirectory? field in ConfigParameters lacks the JSDoc the adjacent plansDirectory? has.

Verified locally: storage.test.ts 67 tests pass and ESLint is clean on the changed sources. (The other two suites failed only on a pre-existing ajv version mismatch in the review sandbox, unrelated to this diff.)

中文版

维持 Request changestodoWrite.ts:422 上那条仍未处理的 [Critical]todosDirectory 只在构造时校验、I/O 前未重新校验/写后未回滚)才是合并阻塞项。

本 PR 自述"完全镜像 plansDirectory",但 plansDirectory 带的若干安全/体验机制没有一并搬过来。三条在上方行内(session-id 净化、legacy 目录迁移告警、文档 .gitignore 提示)。另有三条无法锚定到单一改动行:

  • SDK 读写错位(潜在)readTodosForSession / listTodoSessionstodoWrite.ts:537,545)无参调用 Storage.getTodosDir(),即使项目配置了 todosDirectory 也永远读全局运行时目录。你已在 Risk & Scope 里说明、且当前无仓内调用方,但下一个把它接进 session 恢复/webui 的人会读到空结果。在调用处加一行代码注释,能让这个坑出现在文件里而不只在 PR 描述里。
  • 测试对等plansDirectoryloadCliConfig plansDirectory 测试(packages/cli/src/config/config.test.ts)和 getPlansDir 的三个软链逃逸测试(storage.test.ts)。todosDirectory 两者皆无:没有 CLI 装载/Config 层测试;getTodosDir 套件把 realpath 打桩成真实 fs,只覆盖了字面 ../,软链包含校验那条分支(恰是 :422 关心的)未被测试。
  • 真能解决 #5836 吗? todo 文件名是 ${sessionId}.json,而 sessionId 是每进程新生成的 UUID。把 .qwen/todos 提交进 git 并不能带来 issue 想要的"跨设备恢复任务清单":机器 B 起的是新 session UUID,根本不会读那个提交过去的文件。落盘位置这一半实现了,检索这一半(稳定的、项目级的 key)没有。建议确认这是否在本 PR 范围内,否则 Fixes #5836 名不副实。

次要(不阻塞):文档把默认写成 ~/.qwen/todos,实际解析为 getRuntimeBaseDir()/todos,设了 QWEN_RUNTIME_DIR 就不一样;ConfigParameters 里新增的 todosDirectory? 也缺了相邻 plansDirectory? 那样的 JSDoc。

本地验证:storage.test.ts 67 个用例通过,改动源文件 ESLint 干净。(另两个套件仅因评审沙箱里 ajv 版本不匹配而加载失败,与本 diff 无关。)

— claude-opus-4-8 via Claude Code /qreview


function getTodoFilePath(todoDir: string, sessionId?: string): string {
// Use sessionId if provided, otherwise fall back to 'default'
const filename = `${sessionId || 'default'}.json`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This builds the todo file name from the raw sessionId. The plansDirectory pattern this PR mirrors routes the id through Storage.sanitizePlanSessionId() first (strips /, \, leading dots, Windows-invalid chars) precisely because the session id is untrusted SDK input — a ..-bearing id would otherwise resolve outside the now project-local todosDir and defeat the Storage.getTodosDir containment guard.

Not exploitable today: every sessionId ingress (--session-id, ACP/serve session/load/resume) validates against a UUID / SESSION_FILE_PATTERN regex before it reaches Config, so a traversal id can't arrive. So this is defense-in-depth that plans has and todos drops — suggest a Storage.sanitizeTodoSessionId() mirroring sanitizePlanSessionId, applied here. (Complements the open containment concern at line 422 — a post-write final-path re-check would also catch it.)

— claude-opus-4-8 via Claude Code /qreview

this.targetDir = path.resolve(params.targetDir);
this.plansDirectoryConfigured = Boolean(params.plansDirectory?.trim());
this.plansDir = Storage.getPlansDir(this.targetDir, params.plansDirectory);
this.todosDir = Storage.getTodosDir(this.targetDir, params.todosDirectory);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] When todosDirectory is set, the parallel plansDirectory path warns that files in the legacy location are now unreferenced — addLegacyPlanLocationWarning() (config.ts ~4137, called from the constructor ~1633, surfaced via getWarnings()). There is no addLegacyTodoLocationWarning equivalent, so a user who opts in silently stops seeing their existing ~/.qwen/todos/*.json: no warning, no log, the session just starts with an empty todo list. This contradicts the PR's "migration notes: none."

Suggest mirroring the plans warning — guard on a todosDirectoryConfigured flag, compare Storage.getRuntimeBaseDir()/todos (runtime base, not getGlobalQwenDir()) against the configured dir, and push a "move your existing todo files" warning.

— claude-opus-4-8 via Claude Code /qreview

| `advanced.excludedEnvVars` | array of strings | Environment variables to exclude from project context. Specifies environment variables that should be excluded from being loaded from project `.env` files. This prevents project-specific environment variables (like `DEBUG=true`) from interfering with the CLI behavior. Variables from `.qwen/.env` files are never excluded. | `["DEBUG","DEBUG_MODE"]` |
| `advanced.bugCommand` | object | Configuration for the bug report command. Overrides the default URL for the `/bug` command. Properties: `urlTemplate` (string): A URL that can contain `{title}` and `{info}` placeholders. Example: `"bugCommand": { "urlTemplate": "https://bug.example.com/new?title={title}&info={info}" }` | `undefined` |
| `plansDirectory` | string | Custom directory for approved Plan Mode files. Relative paths are resolved from the project root, and the resolved path must stay within the project root. If unset, plan files are stored in `~/.qwen/plans`. **Requires restart.** If the directory is inside the project root, add it to `.gitignore` to avoid committing plan files. | `undefined` |
| `todosDirectory` | string | Custom directory for todo list files created by the todo-write tool. Relative paths are resolved from the project root, and the resolved path must stay within the project root. If unset, todo files are stored in the global runtime todos directory (`~/.qwen/todos`). **Requires restart.** | `undefined` |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The parallel plansDirectory row (line 465) ends with "add it to .gitignore to avoid committing plan files." This row drops that guidance — yet the example at line 523 uses "todosDirectory": "./.qwen/todos" (inside the project root), which would commit per-session todo JSON (named by session UUID) into the repo.

Suggested change
| `todosDirectory` | string | Custom directory for todo list files created by the todo-write tool. Relative paths are resolved from the project root, and the resolved path must stay within the project root. If unset, todo files are stored in the global runtime todos directory (`~/.qwen/todos`). **Requires restart.** | `undefined` |
| `todosDirectory` | string | Custom directory for todo list files created by the todo-write tool. Relative paths are resolved from the project root, and the resolved path must stay within the project root. If unset, todo files are stored in the global runtime todos directory (`~/.qwen/todos`). **Requires restart.** If the directory is inside the project root, add it to `.gitignore` to avoid committing todo files. | `undefined` |

— claude-opus-4-8 via Claude Code /qreview

clearContextOnIdle: settings.context?.clearContextOnIdle,
fileFiltering: settings.context?.fileFiltering,
plansDirectory: settings.plansDirectory,
todosDirectory: settings.todosDirectory,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This is the only place where the real CLI settings object is wired into Config, but the new todosDirectory pass-through has no integration test. The storage tests cover Storage.getTodosDir() directly, and the tool tests use a mocked getTodosDir(), so a future typo or omission here could make the documented setting silently ignored while those tests still pass. Please add loadCliConfig todosDirectory tests beside the existing plansDirectory block that verify a relative value resolves against cwd via config.getTodosDir() and that ../todos rejects with the containment error.

— GPT-5 via Qwen Code /review

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No additional findings beyond those already raised. The existing Critical from wenshao (missing containment re-check at the I/O site in todoWrite.ts after directory resolution, before the actual file write) and the coverage parity gaps vs plansDirectory (session-id sanitization, symlink-escape tests) are the primary blockers well-covered by the existing CHANGES_REQUESTED review. The known SDK limitation (readTodosForSession/listTodoSessions calling Storage.getTodosDir() with no args) is acknowledged in the PR description.

— claude-sonnet-4-6 via Qwen Code /review


Generated by Claude Code

@@ -533,15 +534,15 @@ Todo list modification failed with error: ${errorMessage}. You may need to retry
export async function readTodosForSession(
sessionId?: string,
): Promise<TodoItem[]> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] readTodosForSession calls Storage.getTodosDir() with no arguments, which always returns the default runtime todos directory. It never consults the user-configured todosDirectory. Session recovery will silently read from the wrong location whenever a custom todosDirectory is set.

Suggested change
): Promise<TodoItem[]> {
Add a `todoDir` parameter: `export async function readTodosForSession(sessionId?: string, todoDir?: string): Promise<TodoItem[]> { return readTodosFromFile(todoDir ?? Storage.getTodosDir(), sessionId); }` callers that have a `Config` instance should pass `config.getTodosDir()`.

— claude-sonnet-4-6 via Qwen Code /review


Generated by Claude Code

/**
* Utility function to list all todo files in the todos directory
*/
export async function listTodoSessions(): Promise<string[]> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] listTodoSessions calls Storage.getTodosDir() with no arguments, which always returns the default runtime todos directory and ignores any configured todosDirectory. A user who sets a custom todosDirectory would see an empty session list from this function.

Suggested change
export async function listTodoSessions(): Promise<string[]> {
Add a `todoDir` parameter: `export async function listTodoSessions(todoDir?: string): Promise<string[]>` and replace the `Storage.getTodosDir()` call with `todoDir ?? Storage.getTodosDir()`. Callers with a `Config` instance should pass `config.getTodosDir()`.

— claude-sonnet-4-6 via Qwen Code /review


Generated by Claude Code

this.targetDir = path.resolve(params.targetDir);
this.plansDirectoryConfigured = Boolean(params.plansDirectory?.trim());
this.plansDir = Storage.getPlansDir(this.targetDir, params.plansDirectory);
this.todosDir = Storage.getTodosDir(this.targetDir, params.todosDirectory);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Missing Config-level test coverage for todosDirectory wiring.

The parallel plansDirectory has a full Config-level test suite in config.test.ts (~lines 5015–5283) covering constructor path resolution, escape-path rejection, and getter behavior. No equivalent exists for todosDirectory — the Storage.getTodosDir unit tests in storage.test.ts cover the helper in isolation, but the Config constructor's threading of params.todosDirectoryStorage.getTodosDir(this.targetDir, ...)this.todosDirgetTodosDir() is untested.

A regression in the constructor (e.g., passing params.todosDirectory in the wrong position, or using this.cwd instead of this.targetDir) would silently produce a wrong directory with no test catching it.

Consider adding two tests mirroring the plansDirectory ones:

  1. Construct Config with todosDirectory: './.qwen/todos' and assert config.getTodosDir() equals path.join(targetDir, '.qwen/todos')
  2. Construct with todosDirectory: '../todos' and assert the constructor throws FatalConfigError

— qwen3.7-max via Qwen Code /review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

在创建任务清单(create todos)能否指定持久化到项目内以便跨设备同步?

4 participants