awam-todo (Input & Manage To-Do)
A personal todo management skill. All data lives in storage/ under this
skill's directory (not tracked by Git), the index lives in index.json
(not tracked by Git), and machine-local environment info lives in
env.json (not tracked by Git, auto-generated by init). The core
command is scripts/todo.py (Python 3).
Documentation layout (this package is bilingual):
SKILL.md— canonical runbook (English).SKILL.zh-CN.md— Chinese mirror of the same runbook.docs/OPTIMIZATION.md— optimization roadmap (English).docs/优化建议.md— the same roadmap in Chinese.
When to use
| User intent | Action |
|-------------|--------|
| First use / setting up on a new machine | init (detect the environment, probe the editor, save config, normalize workspaces) |
| View / modify environment config (path style etc.) | env (--set, --reformat-workspaces) |
| Move the storage location / migrate existing data | env --set storage_dir=<dir> + --migrate (see "Configurable capabilities") |
| Change the editor used for "open in editor" | env --set editor.path=<absolute path or command name> (see "Configurable capabilities") |
| Add a todo | parse → add (with duplicate detection, see below) |
| Check for duplicates before adding | check --text "..." (read-only) |
| Suggest tags read-only (suggest only, never write) | suggest-tags --text "..." (JSON output) |
| View todos | list (filter by state / date) |
| Complete a todo | done <id> (asks for confirmation when prerequisites are unfinished) |
| Mark in-progress / reopen | start <id> (asks when prerequisites unfinished) / reopen <id> |
| Continue working (mark in-progress and open the workspace in the configured editor) | work <id> (alias continue; asks when prerequisites unfinished) |
| Overdue: reschedule / clear the deadline | postpone <id> "next Monday" (aliases reschedule / replan; --clear cancels the deadline) |
| Set / view task dependencies | dep <id> [--add/--remove/--set/--clear] <dep_id...> |
| List subtasks under a parent task | children <id> (alias sub) |
| Add a subtask (belonging to a parent) | add --parent <parent-id> ... |
| Detach a subtask from its parent | deparent <id> |
| Force-archive one day file | archive YYYY-MM-DD |
| Monthly archive (previous months' completed records) | archive-month [--month YYYY-MM] [--all] [--dry-run] (alias monthly-archive) |
| Migrate old storage to the current schema | migrate (preview by default) / migrate --apply |
| View the index (urgent / today / overdue / in_progress / open / done) | index |
| View one task, a day file, or a month archive | show <id|YYYY-MM-DD|YYYY-MM> |
| Triage one sentence (add/update/status/delete) | triage --text "..." (read-only, JSON; exit 3 = needs confirmation) |
| Delete a todo (also cleans dependency/parent references) | delete <id> |
| Capture the clipboard with a global hotkey (no filtering) | Start the web UI (server.py); press the hotkey (default Ctrl+Alt+T); clipboard text feeds the triage entry |
| Manage the capture service (hotkey / triage types / toggles) | Web UI Settings page, or env --set capture.* |
Quick command reference
| Command | Purpose |
|---------|---------|
| add | Add a todo (identical → default to updating; similar → confirm; --force skips checks) |
| check | Read-only duplicate check |
| suggest-tags | Read-only tag suggestion (JSON) |
| done / start / reopen | Complete / mark in-progress / reopen (dependency guard on done & start) |
| work (continue) | Mark in-progress + open the workspace in the configured editor |
| postpone (reschedule, replan) | Reschedule or clear the deadline (overdue exit #2) |
| dep | View / set / append / remove / clear dependencies |
| children (sub) / deparent | List / detach subtasks |
| archive / archive-month (monthly-archive) | Archive a day file / run monthly archiving |
| migrate | Preview or apply schema migration |
| list / show / index | List todos / view a task, day file or month / view the index |
| triage | Read-only triage of one sentence → add/update/status/delete (+parsed fields), JSON; exit 3 when confirmation is needed |
| delete | Delete a todo (also cleans other tasks' dependency/parent references) |
| init / env | Detect & save the environment / view & modify config |
Invocation
python "<skill-directory>\scripts\todo.py" <command> <args>
<skill-directory> is the installation directory of this skill (on Windows
typically C:\Users\<user>\.agents\skills\awam-todo; substitute the actual
install location). Commands below are abbreviated as python ...\todo.py.
Language policy (three layers)
| Layer | Language | Notes |
|-------|----------|-------|
| AI conversation in session | Follows the user's language | Reply in the language the user is speaking (Chinese ↔ English, etc.); when the user writes Chinese, reply in Chinese; when English, in English. Never force one fixed language. |
| CLI / API output (todo.py, server.py) | English, fixed | All user-facing CLI output, argparse help, the tkinter dialog, web API error/message fields and startup logs are English. Data values are exempt: statuses (维护 / 进行中 / 结束 / 待开始 / 其他), importance (重要 / 不重要), urgency (紧急 / 不紧急), tags, view names and the storage-file keys (状态: / 重要: / 内容: …) remain literal Chinese runtime values — pass them to the CLI exactly as written and display them as-is. |
| Web UI (web/index.html) | i18n: English default, switchable to Chinese | Inline i18n word tables (zh/en) with no external libraries, offline-safe. Preference is stored in localStorage under awam-todo:lang; default en. A language selector sits in the top bar (English / 中文). Form <select> values submitted to the backend remain the Chinese data values; only displayed labels/options are translated. Data values from index.json are shown as-is, never translated — e.g. pin_reason is generated in English by the CLI's pinning logic and stays English in the UI. |
Changing the UI language affects display only — never the data. The AI must keep passing Chinese data values (status / importance / urgency / tags / view names) to the CLI in all languages.
Environment detection & path unification (read this first)
awam-todo detects the runtime environment automatically and normalizes
workspace paths to one consistent format.
init — detect and save the environment
Run init on first use or after switching machines:
python ...\todo.py init
It probes the environment and saves it to env.json:
| Field | Meaning |
|-------|---------|
| path_style | Path style: windows / posix / mixed (auto by OS; Windows → windows) |
| platform.os | OS name (Windows / Linux / Darwin …) |
| platform.is_windows | Whether the OS is Windows |
| platform.python_version | Python version |
| platform.hostname / platform.cwd | Hostname / current working directory |
| dirs.skill_dir / dirs.storage_dir | Skill directory / effective storage directory (changeable via env --set storage_dir) |
| editor | The editor used by "open in editor" (path / label / args; written by probing or env --set editor.*) |
init keeps any saved storage directory and editor config (probing never
overwrites them); if editor is unset, it is probed once automatically.
init also normalizes the workspace paths inside existing storage files
to the current path_style (e.g. on Windows, all converted to
G:\Projects\... backslashes).
Windows path unification rules (auto-active on Windows)
- On write (persist): when adding a todo,
--workspaceis normalized toG:\Projects\...(backslash) format before being written, regardless of whether slashes or backslashes were passed. - On read / output:
list/show/index/addalways display workspaces in Windows backslash format. - Workspace only:
--docs,--links(URLs) are never path-converted. - The deciding factor is
env.json'spath_style; when uninitialized, it is inferred from the current OS (Windows → windows).
env — view / modify environment config
python ...\todo.py env # view current config (storage dir + editor)
python ...\todo.py env --set path_style=windows # change path style (windows/posix/mixed)
python ...\todo.py env --reset # re-probe and overwrite (including editor)
python ...\todo.py env --set path_style=posix --reformat-workspaces
# change style and rewrite all workspaces
--reformat-workspaces: normalizes all workspace paths in every storage file to the currentpath_style(use it together with a style change).- Changes are written back to
env.jsonimmediately; after a style change, re-runinitor--reformat-workspacesto sync existing data.
Configurable capabilities (the skill must know these)
The skill has four configurable items, all stored in env.json and all
editable via env --set. When the user mentions "store todos somewhere
else", "I don't use Cursor", "open with <editor X>", "web port", "stop
asking me to start the UI", "change the capture hotkey", or "disable
capture", go through these — do not edit code.
1. Storage directory storage_dir
| Item | Value |
|------|-------|
| Default | storage/ under the skill directory (auto fallback when unset) |
| Authoritative location | dirs.storage_dir in env.json |
| Scope | All reads/writes: day files, archive/ monthly archives, index.json rebuild |
# When the old directory still has todos, you MUST choose one of the two,
# otherwise it errors (exit code 2) and leaves config untouched:
python ...\todo.py env --set storage_dir="D:\awam-todo-data" --migrate # move data too
python ...\todo.py env --set storage_dir="D:\awam-todo-data" --no-migrate # config only, keep data
Migration semantics (_migrate_storage, a hard data-safety constraint —
do not bypass):
- Copy first to the new directory (
YYYY-MM-DD.md+archive/); - Verify that file counts and task-block counts match between old and new;
- Delete old files only after verification passes;
- If any step fails, abort, keep the old directory intact, roll the config back to the old directory, and remove the half-copied files written this run.
Paths support ~ and %VAR% / $VAR environment-variable expansion; relative
paths resolve against the skill directory. Writability is verified before
writing.
2. Editor editor
"Open workspace in editor" (CLI work, web path menu) no longer hard-codes
Cursor — it uses the editor section of env.json:
| Key | Meaning |
|-----|---------|
| editor.path | Required. Absolute path to the editor executable, or a command name on PATH (e.g. code / cursor) |
| editor.label | Optional. Display name for messages and the web menu (e.g. "Open with VS Code") |
| editor.args | Optional. Fixed leading arguments, multiple separated by ; |
python ...\todo.py env --set editor.path="D:\Apps\Cursor\Cursor.exe" # absolute path
python ...\todo.py env --set editor.path=code # or a command on PATH
python ...\todo.py env --set editor.label="VS Code"
python ...\todo.py env --set "editor.args=--new-window"
python ...\todo.py env --set ui.port=9000 # web UI port (default 8796, see item 3)
python ...\todo.py env --set ui.prompt=off # stop asking to start the web UI (see item 3)
init/env --resetauto-probe common editors (Cursor, VS Code, Trae, Windsurf, Sublime Text, Zed). The probe tableEDITOR_CANDIDATESis a constant intodo.py; to add another editor, add one entry.- If nothing is found,
editoris left unset for you to specify. - Unset or invalid paths always error and disable the entry point: the
CLI
workprints an error plus a copyable config command (its primary job, "mark in-progress", still completes with return code 0); the web does not render the "open in editor" menu item. Do not degrade into "guess a path from the software name" or "silently open with the system default". - Launching the editor detaches stdio and the process group so the editor never holds the caller's pipe.
3. Web UI service ui
Switch and port for "after add/maintain, check the local web service and ask whether to start the UI":
| Key | Meaning |
|-----|---------|
| ui.port | Web service port, default 8796 (matches web/server.py default) |
| ui.prompt | ask (default) / off. ask = after a successful mutation, if the service is not running, ask whether to start it; off = never check, never ask |
python ...\todo.py env --set ui.port=9000 # change port (probe & launch at this port when down)
python ...\todo.py env --set ui.prompt=off # disable "ask to start the web UI after mutations"
- Temporary override (self-test / CI / scripted use): set the environment
variable
AWAM_TODO_UI_PROMPT=off; it takes precedence overenv.json. - This config only affects the check/ask side in
todo.py; the web service's own port still follows its launch argument (server.py --port).
4. Capture service capture
Global hotkey + clipboard capture that feeds the triage entry, running inside the web UI process (see "Capture service" below for the full flow):
python ...\todo.py env --set capture.enabled=false # stop listening with the UI
python ...\todo.py env --set capture.hotkey="Ctrl+Shift+X" # change the hotkey (re-registers on UI start)
python ...\todo.py env --set capture.confirm_delete=false # delete without asking (not recommended)
python ...\todo.py env --set capture.toast=false # no corner toast
python ...\todo.py env --set capture.triage_types.delete=false
# disable delete triage (falls back to Add + ask)
env output prints the current capture configuration line
(Capture service (hotkey reads clipboard -> triage): …). Invalid hotkeys
and unknown sub-keys are rejected with exit 2.
Parsing a todo in session (the core of add)
When the user states a todo in natural language, extract the following fields
and pass them to add; unspecified values get their defaults.
| Field | Argument | Parse rule | Default |
|-------|----------|------------|---------|
| Task text | --text | Required; the core thing the user wants done | — |
| Importance | --importance | 重要 / 不重要 | 不重要 |
| Urgency | --urgent | 紧急 / 不紧急 | 不紧急 |
| Status | --status | 维护 / 进行中 / 结束 / 待开始 / 其他 | 进行中 |
| Due time | --due | Auto-parsed time (see mapping below); if not mentioned → no deadline | none |
| Note | --note | Extra explanation from the user | empty |
| Blocker (contingency) | --blocker | The obstacle most likely to stop me; optional; shown directly on cards and lists | empty |
| Counter (contingency) | --counter | "If it happens, I will…"; optional; pairs with the blocker | empty |
| Tags | --tags | Category tags, multiple separated by ; (e.g. 工作;学习); when omitted the script suggests but does not write (see below) | empty |
| Workspace dir | --workspace | A project/directory the user names (e.g. "put it in the XX project") | empty |
| Related docs | --docs | Related files/docs, multiple separated by ; | empty |
| Links | --links | Related URLs, multiple separated by ; | empty |
| Dependencies | --deps | Prerequisite task IDs, multiple separated by ; (e.g. T-A;T-B); must exist and be acyclic | empty |
| Parent task | --parent | The parent task this task belongs to (as its subtask); must exist, no cycles | empty |
| Source | --from-type / --from-url | Source type (agent/link/web) + source link (e.g. an AI conversation link); URL left empty → not recorded | none |
| Storage date | --date | When the user says "put it on day X" → store in that day's file | today |
Note: statuses, importance, urgency, tags and view names are literal Chinese runtime values — pass them to the CLI exactly as written above; the English in this document is only the explanation.
Duplicate detection (always runs on add)
Every add first scans all storage and compares normalized task text:
| Result | Script behavior | What the agent MUST do |
|--------|-----------------|------------------------|
| Identical (normalized text matches fully) | No new task, exit code 3, lists the matched task | Confirm with the user; the default is to update the existing task's status (recommend --update-id <id>), do not create a duplicate |
| Similar (similarity ≥ ~72%, or one clearly contains the other) | No new task, exit code 3, lists the matched task | Confirm with the user: update a specific ID / still create / cancel |
| No match | Creates normally | Report the result |
| User confirmed the update | add --update-id <id> ... | Without --status, the script sets the status to 「进行中」; other fields are only overwritten when explicitly passed |
| User insists on creating | add --force ... | Skip duplicate detection and force-create |
Confirmation wording (identical):
An identical todo already exists:
T-…(进行中): …… By default I would update that task's status instead of creating a new one. Confirm the update? Or force-create / cancel?
Confirmation wording (similar):
Found a similar todo
T-…(similarity xx%): …… Please choose: update that one / still create / cancel.
Read-only precheck: check --text "..." (also returns exit 3 on a hit).
Tag suggestion (AI-created todos: suggest only, never write)
The AI creating todos in session is the primary creation path, and tags are often not stated by the user. To avoid "created it and only then realized it wasn't categorized", the script can suggest tags — but it never writes them on your behalf: tags are the user's own classification scheme; suggestions are only suggested values to confirm.
| Scenario | Script behavior | What the agent MUST do |
|----------|-----------------|------------------------|
| add without --tags (or empty) | After the suggestion, prints 建议标签: …(尚未写入;确认后用:add --update-id <id> --tags "…") and 依据: … | Repeat the suggestion to the user and confirm; only then persist with --update-id |
| add with explicit --tags | No suggestion; writes the passed value | Nothing extra |
| add --update-id updating an existing task | No suggestion (early return) | Nothing extra |
| Suggestion is uncertain | Returns empty, shows nothing | Don't press the user, and don't invent a tag |
| Ask first, then create (recommended) | suggest-tags --text "..." outputs JSON with suggestions / details / reason / written:false | Get suggestions, ask the user, then create with one add --tags "…" |
Evidence used (three kinds, combinable; only shown when confidence passes):
- Historical tag reuse: scans existing tasks in
storage/; reuses tags of historical tasks whose text is sufficiently related; - Content/note keywords: word-table rules (学习 / 英语 / 开发 / 工作 / 文档 / 健康 / 生活 — seven categories);
- Workspace domain words: domain words in the path (e.g.
src/dev/repo,course/learn,doc/wiki) — auxiliary evidence only; never enough alone for a suggestion.
English matches use word boundaries (debug won't be misjudged as 开发
because it contains bug). The word tables and thresholds are Chinese
constants declared together at the top of scripts/todo.py
(TAG_RULES / TAG_WORKSPACE_RULES / TAG_MIN_SCORE etc.) — edit the words
or thresholds directly.
Iron rule, continuing this skill's no-invention principle: when unsure, give no suggestion — better the user fills it in than a plausible-looking tag is inserted.
Dependency chains (hard prerequisite check)
Tasks can carry prerequisite dependencies (other todo IDs), forming dependency chains. The dependency check itself is a dead hard rule; when it conflicts, the AI recommends how to proceed (executed after user confirmation).
Setting dependencies (referenced IDs must exist; existence and cycle detection run — self-reference, cycles, or missing references error out):
python ...\todo.py dep T-20261002-005 # view dependencies
python ...\todo.py dep T-20261002-005 --add T-20261002-001 T-20261002-003 # append
python ...\todo.py dep T-20261002-005 --set T-20261002-001 # set (replace)
python ...\todo.py dep T-20261002-005 --remove T-20261002-001 # remove
python ...\todo.py dep T-20261002-005 --clear # clear
You can also carry dependencies at creation: add --deps "T-A;T-B".
Hard check rule (dead): when advancing a target to 进行中 (start /
work) or 结束 (done), if not all direct prerequisites are 结束,
the script does not persist, prints the conflict list, and returns exit
3 (the same "needs confirmation" signal as duplicate detection) prompting
the user to confirm. reopen (back to 待开始) is not subject to dependency
constraints.
Conflict handling (AI recommends): when the script returns exit 3 and
the output says "dependency check failed", the agent should give the user a
recommendation with options, e.g.:
This task depends on
T-…(待开始): ……, which is not finished yet, so it cannot advance to 「进行中/结束」. Please choose: ① ignore dependencies and force advance (--force) ② finish the prerequisite(s) first ③ cancel.
- User confirms force advance → agent re-runs with
--force(done <id> --force/start <id> --force/work <id> --force). - Once prerequisites are
结束, later advances pass automatically without further confirmation. - Index entries carry a
blockedfield (whether any prerequisite is unfinished);listmarks such tasks "(被依赖阻塞)".
Note: add's --force is the single override switch for both "skip
duplicate detection" and "skip the dependency hard check"; without --force,
a new task starting at 进行中/结束 with unfinished prerequisites is also
blocked.
Subtasks (belonging to a parent)
A subtask belongs to one parent task; all other attributes (status / importance / urgency / note / workspace / docs / links / due / dependencies) are identical to ordinary tasks. A task can have a parent and its own subtasks (nestable, but parent-child chains must not cycle).
python ...\todo.py add --text "打包模块" --parent T-20261002-001 # add a subtask (to a parent)
python ...\todo.py children T-20261002-001 # list subtasks of a parent
python ...\todo.py sub T-20261002-001 # same (alias)
python ...\todo.py deparent T-20261002-005 # detach from parent
python ...\todo.py show T-20261002-005 # view (includes 父任务 field)
Parent-completion hard check (dead): marking a task that still has
unfinished subtasks as 结束 (done) does not persist; the script prints
the unfinished-subtask list and returns exit 3 (same "needs confirmation"
signal as dependency/duplicate checks) for the AI to confirm with the user.
Conflict handling (AI recommends):
This parent task still has unfinished subtasks
T-…(进行中): ……, so it cannot be marked 「结束」. Please choose: ① ignore subtasks and force-finish (--force) ② finish the subtasks first ③ cancel.
- When all subtasks are finished, the parent can complete normally; when the
user confirms force-finishing, run
done <id> --force. --parentat creation validates that the parent exists, is not a self-reference, and has no cycle; updating a task's parent (viaadd --update-idor the web UI) validates the same way.- Index entries carry a
parent(parent task ID) field.
Time parsing (--due is handed to the script, parsed by these rules)
- 今天 / 今日 → today; 明天 / 明日 → tomorrow; 后天 → the day after tomorrow
- 星期X / 周X → the next occurrence from now; 下周X → that weekday next week
X月X日,2026-10-05→ the specified dateHH:MM,X点,X点半→ that time today- Relative date + time combos:
明天 10:00,周五 09:00,3天后 15:00 X小时后/X分钟后→ computed from the current moment- Parse failure → do not pass
--due(no deadline), and tell the user in the reply "no time recognized, no deadline set".
Examples
User: 记一下,明天上午十点前写好季度报告,重要,备注里带上Q3数据,放到 19AI 项目,
参考 data.xlsx,附上 https://example.com/report,标签 工作;汇报
→ python ...\todo.py add --text "写好季度报告" --importance 重要 --due "明天 10:00" \
--note "带上Q3数据" --workspace "G:\Projects\19AI" --docs "data.xlsx" \
--links "https://example.com/report" --tags "工作;汇报"
# If exit 3 and the task is identical (T-20261002-001), and the user confirms "update":
→ python ...\todo.py add --update-id T-20261002-001 --text "写好季度报告" --importance 重要 \
--due "明天 10:00" --note "带上Q3数据" --workspace "G:\Projects\19AI"
# If the user insists on creating:
→ python ...\todo.py add --force --text "写好季度报告" ...
Triage entry (triage) — one sentence → add / update / status / delete
A read-only, pure-rule entry that decides what a sentence means for the todo list. No LLM involved — the AI conversation may use it directly (e.g. the user pastes a random sentence), and the capture service feeds it from the clipboard. It never writes anything by itself.
python ...\todo.py triage --text "把继续detect-agent软件的开发标记为完成" # JSON on stdout
python ...\todo.py triage --text "..." > triage.json
echo %ERRORLEVEL% # 0 = decisive, 3 = needs confirmation
Decision order: delete > status > update > add. Questions ("…?") and
utterances with no actionable verb fall back to unknown + need_confirm.
Promises with lead-ins like 需要/要/必须/应该/记得 that target no existing
task are forced back to add (a commitment to a new task). Fields extracted
by rule: due (明天/下周一/3天后/HH:MM…), important/urgent (重要/紧急),
备注:, 标签:, workspace paths, and update/delete subjects. The target is
resolved against real tasks by T-ID or by text similarity (prefers unfinished
tasks); when a target cannot be locked with enough confidence, the result is
need_confirm with candidates (or a fallback_action=add).
JSON contract (cmd_triage output):
| Field | Meaning |
|-------|---------|
| action | add / update / status / delete / unknown |
| confidence | high / medium / low |
| need_confirm | whether the caller must confirm before executing (destructive delete always true) |
| reasons | why confirmation is needed (destructive / question / duplicate / target_not_found / action_disabled …) |
| text | the original sentence, verbatim (no rule filtering — this is the captured task content) |
| target / candidates | matched task {id, text, score, kind} (kind: exact / similar) |
| status | target status for a status action (结束/进行中/待开始/维护/其他) |
| fields | parsed extras: due / importance / urgent / note / tags / workspace / text (for updates) |
| suggested_tags | read-only tag suggestions for add (never written) |
| fallback_action | what to propose when the target is missing (usually add) |
AI usage in session: run triage --text "<the user's sentence>"; if
need_confirm is true (or action=delete), confirm with the user first, then
execute the underlying command (add / done/start for status / add --update-id / delete). If need_confirm is false and action=add, proceed
with add and the parsed fields.
Capture service (global hotkey → clipboard → triage → system dialogs)
A no-filter capture: whatever text is on the clipboard when the hotkey is
pressed is handed verbatim to the triage entry as the task content. No
keyword filtering, no length check — the raw clipboard text is the task
content. The service runs inside the web UI process (web/server.py), so
it is started/stopped together with the UI and configured from the UI's
Settings page.
- Start: launch the web UI (
python ...\web\server.py). On startup it registers the hotkey (defaultCtrl+Alt+T) and printsCapture service: listening: …. - Press the hotkey: the service reads the clipboard →
triage_text→ applies the enabled triage types → executes:need_confirm=false(high-confidence add/status/update) → executes directly, shows a corner toast (toggleable);- otherwise → a system dialog (tkinter) pops up showing the verdict and asking for confirmation / letting the user fill fields (deadline, tags, note, importance/urgency, target choice, new status);
- delete always asks (toggleable via
capture.confirm_delete); - a blocked operation (dependencies/subtasks/duplicate) pops a second dialog offering "Force".
- Dialog language: fixed English labels; Chinese data values (status / importance / urgency / tags / original text) are shown verbatim.
- Stop: stop the web UI process (Ctrl+C) — the hotkey listener dies with it.
Configuration (all in env.json under capture, managed from the web UI
Settings page or env --set capture.*):
| Key | Meaning |
|-----|---------|
| capture.enabled | true (default) / false — start the hotkey listener with the UI |
| capture.hotkey | Ctrl+Alt+T (default). Modifiers Ctrl/Alt/Shift/Win + F1–F24, a letter/digit, Space, Enter, Tab, Esc; at least one modifier required |
| capture.confirm_delete | true (default) — always confirm before deleting from the capture dialog |
| capture.toast | true (default) — corner toast after each capture |
| capture.triage_types | {add, update, delete, status} all true by default; a disabled action always asks and falls back to add |
The Settings page also offers a Try triage box: paste a sentence, see the
JSON verdict read-only (POST /api/capture/triage) — nothing is written or
executed.
Contingency plans: blocker + counter (optional, but recommended)
Each todo can carry an implementation intent (if-then) pair, persisted as
障碍: / 对策: lines and exposed in the index as blocker / counter:
python ...\todo.py add --text "背完 2000 个单词" \
--blocker "晚上刷手机" --counter "拿起手机前先背 20 个"
- Both fields are optional and can be filled independently; web cards put the plan right at the decision point, not hidden in an edit form.
- When reporting/showing a task, if the user hasn't filled them, don't invent a blocker or counter — show 「(未填)」.
- When the user says things like "I keep procrastinating / I scroll my phone at night" in session, proactively suggest writing them into these fields.
Overdue, pinning, and two exits
- Overdue days: only when 「unfinished + deadline passed (by calendar
day)」 → show
逾期 N 天; no deadline, completed, or not yet overdue never shows the number (do not show 「逾期 0 天」). - Stall days: 进行中 and ≥
STALL_DAYS(default 3) since last update (or creation) →已 N 天未推进,滚入今日. - Pin order: overdue (red) > stalled (amber) > due today (blue). Every
pin carries a reason (
pin/pin_reasonin the index) — no silent reordering. - Overdue must offer both exits (hard requirement):
- Advance now →
start <id>/done <id> - Adjust the plan →
postpone <id> "下周一"(reset the deadline) orpostpone <id> --clear(remove the deadline)
- Advance now →
python ...\todo.py postpone T-20261002-001 "下周一 10:00" # reschedule
python ...\todo.py postpone T-20261002-001 --clear # remove the deadline
python ...\todo.py list --state overdue # only overdue (with days + exit hints)
python ...\todo.py list --state today # what to handle today (overdue/stalled/due today)
When a time can't be parsed, postpone errors and keeps the original value —
never guess a date.
No-invention principle (important)
Derived metrics return None when input is missing, rendered as — or
暂无推算:
| Metric | With data | Without data |
|--------|-----------|--------------|
| Overdue days | 逾期 N 天 | field absent |
| Days until due | N 天 | — |
| Stall days | N 天 | — |
| Projected finish | needs ≥ 3 progress-record samples to project | 暂无推算(缺少进展记录) |
The current version has no progress-record layer, so projected finish is
always 「暂无推算」. This is deliberate — the record-layer design is in
docs/OPTIMIZATION.md (items 7/8). Never output fake numbers just to look
complete.
Week boundary (fixed — do not change on a whim)
Any weekly statistic uses Monday 00:00 through Sunday 23:59
(WEEK_START_ISO = 1). The index's week section provides
start / end / created / done. Future weekly comparisons must use the
same boundary.
Storage schema version & migration
- Every storage file starts with
schema: N(currentSCHEMA_VERSION = 2);index.jsoncarriesschema_versionat the top level. - Old files without a
schemaline are treated as v1;migratepreviews by default and only--applyrewrites (read and write back in the current format, filling in the schema header and defaults for new fields, lossless). - When adding a field: update
_parse_block/_write_block/_task_view/build_indextogether and bumpSCHEMA_VERSION, otherwise the web front-end won't see the new field.
Monthly archiving
When a new month begins, day files from previous months and earlier whose
tasks are all finished are merged into storage/archive/YYYY-MM.md and the
original day files are deleted; files with unfinished tasks stay put.
Archive files carry 月份: / 来源文件: headers and each task carries an
原日期: line; viewable via show YYYY-MM. Archive files do not participate
in daily indexing — they are only counted in index.json's
monthly_archives / summary.archived_tasks.
The automatic trigger lives in build_index() (runs once per process; read
commands can also trigger it — expected behavior); manual trigger is
archive-month, with --dry-run to preview first.
Overridable defaults (prefill only, not constraints)
The defaults section of env.json controls the default status /
importance / urgency for new todos:
python ...\todo.py env --set defaults.status=待开始
python ...\todo.py env --set defaults.importance=重要
The web creation form prefills with the same values; the user can change them anytime. Defaults are suggested values, not constraints.
Data & state
- Storage:
storage/YYYY-MM-DD.md; each file has归档: true/false,schema: Nheaders plus task blocks (## T-YYYYMMDD-NNN+状态/重要/紧急/内容/父任务/备注/障碍/对策/工作空间/文档/链接/标签/依赖/来源/截止/创建/更新lines).依赖: T-A; T-Brecords prerequisite task IDs;父任务: T-Xrecords the parent task;标签: 工作; 学习records category tags (;-separated). - Source
from: optional object{type, url}recording where the todo came from (e.g. an AI agent conversation link), written via--from-type/--from-url; when the conversation link is unavailable, leave it empty (not persisted, not indexed) — never invented. - ID:
T-YYYYMMDD-NNN, incremented within the day, auto-generated. - Status:
维护 [ ]·进行中 [~]·结束 [x]·待开始 [ ]·其他 [ ]; new todos default to 「进行中」,openno longer appears. - Importance:
重要/不重要(default 「不重要」). - Urgency:
紧急/不紧急(default 「不紧急」); a deadline within 24 hours or already passed is still auto-treated as urgent. - Archive invariant: when all tasks in a file are
done, the file is auto-marked归档: trueand recorded in the index'sarchived_files; reopening a task un-archives the file if it is no longer fully complete.
Index (index.json, auto-rebuilt after every change)
{ "schema_version": 2,
"generated": "...",
"summary": {"todo":n, "in_progress":n, "urgent":n, "done":n, "total":n,
"archived_files":n, "files":n, "today":n, "overdue":n,
"week_created":n, "week_done":n, "monthly_archives":n, "archived_tasks":n},
"week": {"start":"YYYY-MM-DD", "end":"YYYY-MM-DD", "created":n, "done":n},
"urgent": [...], "today": [...], "overdue": [...],
"in_progress": [...], "todo": [...], "done": [...],
"archived_files": ["YYYY-MM-DD", ...],
"monthly_archives": [{"month":"YYYY-MM","files":[...],"file_count":n,"tasks":n}] }
Index entries include depends_on (prerequisite ID list), blocked
(whether any prerequisite is unfinished), parent (parent task ID),
tags (tag list), blocker / counter (contingency plan), and derived
metrics overdue_days / due_in_days / stall_days / projected_finish /
pin / pin_reason (all null when there is no data; render as
「—」/「暂无推算」).
Sort rule: when presenting progress to the user (list / index),
unless the user says otherwise, order by
"pinned (overdue > stalled > due today) → urgent → importance (重要>不重要) →
has deadline → creation time" (implemented in the script).
When reporting, prefer a one-line overview from index's summary and these
buckets: look at today (what to handle today) and overdue first, then
in_progress; whenever mentioning overdue, also give the two exits
(advance / reschedule).
Front-end implementation constraints (long-term)
When editing web/index.html, always follow:
- Zero external libraries, zero CDN, zero external fonts/images — all CSS/JS/icons inline, usable offline.
- Icons are inline SVG (the
Picon table in the file + theic(name, cls)helper), no emoji. - Show 「—」/「暂无推算」 when data is insufficient — no filling zeros, no invented dates.
- Destructive operations (delete) need a second confirmation; pin/ordering changes must be explainable.
- Hover/overlay positioning: anchor vertically to the trigger element,
follow the mouse only horizontally. The overlay must not follow the
mouse entirely (the mouse would never catch it); in multi-card scenes take
"the overlay of the currently hovered card", never
document.querySelector()for the first one in the document. Bridge the gap between overlay and trigger with a transparent bridge band so:hoverdoesn't break and the overlay doesn't vanish when the mouse passes over. - After changes run both self-tests:
python scripts/selftest.py(CLI scenarios, temp dirs, never touches real data) andnode scripts/smoke-web.js(front-end render smoke, minimal DOM stubs). - i18n is inline and self-contained: the zh/en word tables live in the
script (constant
I18N, helpertr()), language preference useslocalStorage["awam-todo:lang"], defaulten; never pull an external i18n library or CDN (offline constraint). Display-only:<select>values and stored data stay Chinese.
Non-goals
- No notes, calendar, or project scheduling; only todo management itself.
- No multi-user collaboration / sync / remote services.
fromsource links can only be obtained by the agent from session context (e.g. the current AI conversation link); when unavailable, leave it empty — the script never invents one.
Web front-end (web/)
When the user asks to "view todos on a web page / open the board / web view / operate todos in the browser", start the local web front-end:
start-awam-todo.cmd # single entry: stop stale server, start UI backend + capture service together
powershell -NoProfile -ExecutionPolicy Bypass -File "scripts\start-awam-todo.ps1" [-NoBrowser]
python "...\awam-todo\web\server.py" # direct; the capture service starts inside the same process
python "...\awam-todo\web\server.py" --port 9000 # specify a port
- The recommended single entry is
start-awam-todo.cmd(or the.ps1it calls): it first stops any stale awam-todo server (a leftover process that holds the port — including ones launched with a relativeweb\server.pypath), waits for the port to free, then starts the web UI backend and the capture service together in one process, and finally self-checks/api/settingsprinting the capture-service state (Capture service state: running). If the UI previously showed 「服务未运行 / 加载失败」in the Settings page, that usually means an old pre-upgrade server still held the port — re-run this entry to fix it. - The service stays in the background; browse to
http://127.0.0.1:8796/. - Auto check & ask after mutations (LLM / CLI both): after a successful
add / maintain of a todo (
add/add --update-id/done/start/work/reopen/postpone/depchange /deparent), the script checks whether the local web service is running — if not running, it pops a dialog asking 「是否现在启动网页 UI」: on "start", it launchesweb/server.pyin the background and opens the browser; on "skip", it does nothing. If the service is already running, no interruption.- Disable the prompt:
env --set ui.prompt=off(temporary override: environment variableAWAM_TODO_UI_PROMPT=off). - Agent note: the dialog is popped by the script itself; after the user
clicks, the script launches the service on its own — the agent only
reports the script's output. If the script reports "cannot pop a dialog"
(no GUI), the agent should confirm with the user in session and then run
the
server.pystart command.
- Disable the prompt:
- The web front-end supports: list view & stats, filter by status/tag, keyword search, full CRUD, status transitions (start/complete/reopen/ 待开始), duplicate-detection confirmation (identical/similar), dependency & subtask conflict confirmation (force-advance allowed).
- Views:
全部 / 今日 / 进行中 / 待开始 / 已结束 / 紧急 / 逾期. 「今日」 = overdue + stalled + due today, each with its pin reason, with a one-line definition at the top. 「今日」 is a view, not the landing entry — the default landing is 「全部」. - Contingency plans: create/edit can fill 障碍/对策 (single-line text,
no
;needed); cards display them directly; search also matches their content. - Overdue two exits: overdue cards directly show 「立即推进」 and 「调整计划」 buttons; 「调整计划」 pops an input supporting natural-language times (明天 / 下周一 / 3天后), plus a one-click clear-deadline action.
- Three responsive tiers: phone (<640) single column + bottom tabs; tablet (640–1023) top horizontal nav; desktop (≥1024) fixed left sidebar (views/tags/definition info) + multi-column card grid.
- All icons are inline SVG, no emoji; semantic three-state colors: green=done, red=overdue, amber=stalled/fallback (distinct from error).
- Both-end creation entries: top 「+新增待办」 button + a large dashed 「新增待办」 card at the bottom of the list.
- Tags: create/edit can fill tags (
;-separated); cards show#标签badges; the toolbar has a tag dropdown filter (with per-tag counts). - Workspace hover menu follows the mouse: hovering a project path, the action menu (copy path / open dir / open in editor) pops centered at the mouse position (horizontal center, slightly below the cursor) and follows the mouse; once the menu is entered it locks for clicking.
- 「Open in editor」 renders per config: the menu label comes from
env.json'seditor.label(e.g. 「用 VS Code 打开」); wheneditoris unset or the path is invalid, the entire menu item is not rendered (no dead clicks). Backend endpoint:POST /api/workspace/editor(/api/workspace/cursoris the legacy alias). The web only changes the label — no config UI; configuration always goes through CLIenv --set. - Delayed save (patch mechanism): web operations no longer write files
in real time; all edits are staged in browser memory + browser storage
(
localStorage:awam-todo:baseline/awam-todo:pending) as a patch queue; on save, onePOST /api/todos/apply-patchescompares against the file as of that moment and applies patches before persisting, avoiding concurrent-write conflicts with the CLI. Change comparison before saving: the content to write is compared character-by-character with the current disk content; when identical (e.g. submitting the same field value, same status, deleting an already-deleted task), no file is written and no index rebuild happens (responsechanged=0), avoiding needless rewrites. Save triggers: ① clicking 「保存更改」 on the page ② auto-save (interval configurable on the page, default 10 min, stored inawam-todo:autosave-min) ③ on close/refresh, autosendBeaconsave. Unsaved changes show a badge count; if there were unsaved changes at last close, reopening prompts to restore. - The backend reuses
todo.py's parse / validate / index logic; when the CLI changed files, the web merges into the latest file on save (external_changed=trueprompts 「检测到外部修改,已合并保存」). - Stop: press Ctrl+C in the running window (or end the corresponding python process).
REST API (local machine only):
GET /api/todoslist + summary (?state=,?q=,?tag=filters; includesrevisionfile fingerprint)GET /api/todos/<id>single taskPOST /api/todoscreate (duplicate hit → 409 + duplicates;force/update_idsupported)PUT /api/todos/<id>update fields (incl. status, with dependency/subtask guards)PATCH /api/todos/<id>/statusstatus only (conflict → 409 + conflicts;forcesupported)DELETE /api/todos/<id>delete (also cleans other tasks' references to it: dependencies / parent)POST /api/todos/apply-patchesapply a patch batch and persist once (web delayed-save entry) body:{"revision": <fingerprint from GET /api/todos>, "patches": [{"op":"create|update|status|delete", ...}]}; before saving, the revision is compared with the current file fingerprint; on mismatch the patches are applied to the latest file (merge semantics), and the response includesexternal_changed,failed(duplicate/conflict/not_found),id_map(temp_id → official ID), andchanged(number of files actually written; 0 = content identical to disk, nothing written).POST /api/workspace/openopen a directory with the system file managerPOST /api/workspace/editoropen a directory with the editor configured inenv.json(/api/workspace/cursoris the legacy alias)GET /api/settingscapture-service config + current service statePUT /api/settingsupdate capture settings ({capture:{enabled,hotkey,confirm_delete,toast,triage_types}}); re-registers the hotkey immediately, response includes the new service statePOST /api/capture/triagerun the triage entry on{text}— same JSON contract as the CLItriagecommand (read-only)
If an old service already occupies the port at startup, use the single entry
(start-awam-todo.cmd) which stops the stale server automatically before
restarting. Web changes and CLI todo.py share the same
source; whichever side operates last, the other side always sees the latest
state.
微信扫一扫