diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml new file mode 100644 index 0000000..01be571 --- /dev/null +++ b/.github/workflows/docs-ci.yml @@ -0,0 +1,58 @@ +name: docs-ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + docs-ci: + runs-on: ubuntu-latest + # Job-level env so step-level `if:` expressions can read CLI_SOURCE. + # Step-level env is *not* visible when the step's `if:` is evaluated. + env: + CLI_SOURCE: ${{ secrets.CLI_SOURCE_PATH }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install + run: pnpm install --frozen-lockfile=false + + # `check:source-commits` and `check:fuzzy-quantifiers` only read files + # already in this repo, so they always run on PR. + - name: check source-commit alignment (§0.3) + run: pnpm check:source-commits + + - name: lint fuzzy quantifiers (§0.4) + run: pnpm check:fuzzy-quantifiers + + # `gen:appendix` + `check:orphans` need the CLI source tree. We expose + # a `CLI_SOURCE` env var via repo secret (job-level env above); if + # absent (e.g. fork PRs), those steps are skipped with a warning so + # external contributors are not blocked. Internal PRs that touch + # chapter content will run the full sweep on the maintainer push + # to `main`. + - name: regenerate appendix manifests (§7.1) and check drift + if: ${{ env.CLI_SOURCE != '' }} + run: | + pnpm gen:appendix --diff-summary + # Working tree must be clean — drift means the doc PR forgot to + # rerun `pnpm gen:appendix`. + git diff --exit-code docs/appendix + + - name: orphan check (§7.6) + if: ${{ env.CLI_SOURCE != '' }} + run: pnpm check:orphans + + - name: skip-source warning + if: ${{ env.CLI_SOURCE == '' }} + run: | + echo "::warning ::CLI_SOURCE not configured; skipped gen:appendix + check:orphans. A maintainer push will exercise the full sweep." diff --git a/docs/appendix/A.manifest.json b/docs/appendix/A.manifest.json new file mode 100644 index 0000000..04e8822 --- /dev/null +++ b/docs/appendix/A.manifest.json @@ -0,0 +1,514 @@ +{ + "generated_at": "2026-05-21T17:10:03.796Z", + "source_commit": "290fdc9481a70612bc5823aa4ed225c52c52aad3", + "appendix": "A", + "items": [ + { + "name": "AgentTool", + "category": "runtime-leaf", + "source_files": [ + "tools/AgentTool/agentColorManager.ts:1-66", + "tools/AgentTool/agentDisplay.ts:1-104", + "tools/AgentTool/agentMemory.ts:1-177", + "tools/AgentTool/agentMemorySnapshot.ts:1-197", + "tools/AgentTool/AgentTool.tsx:1-1398", + "tools/AgentTool/agentToolUtils.ts:1-686", + "tools/AgentTool/built-in/claudeCodeGuideAgent.ts:1-205", + "tools/AgentTool/built-in/exploreAgent.ts:1-83", + "tools/AgentTool/built-in/generalPurposeAgent.ts:1-34", + "tools/AgentTool/built-in/planAgent.ts:1-92", + "tools/AgentTool/built-in/statuslineSetup.ts:1-144", + "tools/AgentTool/built-in/verificationAgent.ts:1-152", + "tools/AgentTool/builtInAgents.ts:1-72", + "tools/AgentTool/constants.ts:1-12", + "tools/AgentTool/forkSubagent.ts:1-210", + "tools/AgentTool/loadAgentsDir.ts:1-755", + "tools/AgentTool/prompt.ts:1-287", + "tools/AgentTool/resumeAgent.ts:1-265", + "tools/AgentTool/runAgent.ts:1-973", + "tools/AgentTool/UI.tsx:1-872" + ] + }, + { + "name": "AskUserQuestionTool", + "category": "runtime-leaf", + "source_files": [ + "tools/AskUserQuestionTool/AskUserQuestionTool.tsx:1-266", + "tools/AskUserQuestionTool/prompt.ts:1-44" + ] + }, + { + "name": "BashTool", + "category": "runtime-leaf", + "source_files": [ + "tools/BashTool/bashCommandHelpers.ts:1-265", + "tools/BashTool/bashPermissions.ts:1-2621", + "tools/BashTool/bashSecurity.ts:1-2592", + "tools/BashTool/BashTool.tsx:1-1144", + "tools/BashTool/BashToolResultMessage.tsx:1-191", + "tools/BashTool/commandSemantics.ts:1-140", + "tools/BashTool/commentLabel.ts:1-13", + "tools/BashTool/destructiveCommandWarning.ts:1-102", + "tools/BashTool/modeValidation.ts:1-115", + "tools/BashTool/pathValidation.ts:1-1303", + "tools/BashTool/prompt.ts:1-369", + "tools/BashTool/readOnlyValidation.ts:1-1990", + "tools/BashTool/sedEditParser.ts:1-322", + "tools/BashTool/sedValidation.ts:1-684", + "tools/BashTool/shouldUseSandbox.ts:1-153", + "tools/BashTool/toolName.ts:1-2", + "tools/BashTool/UI.tsx:1-185", + "tools/BashTool/utils.ts:1-223" + ] + }, + { + "name": "BriefTool", + "category": "runtime-leaf", + "source_files": [ + "tools/BriefTool/attachments.ts:1-110", + "tools/BriefTool/BriefTool.ts:1-204", + "tools/BriefTool/prompt.ts:1-22", + "tools/BriefTool/UI.tsx:1-101", + "tools/BriefTool/upload.ts:1-174" + ] + }, + { + "name": "ConfigTool", + "category": "feature-gated", + "source_files": [ + "tools/ConfigTool/ConfigTool.ts:1-467", + "tools/ConfigTool/constants.ts:1-1", + "tools/ConfigTool/prompt.ts:1-93", + "tools/ConfigTool/supportedSettings.ts:1-211", + "tools/ConfigTool/UI.tsx:1-38" + ], + "feature_flags": [ + "env:USER_TYPE" + ] + }, + { + "name": "EnterPlanModeTool", + "category": "runtime-leaf", + "source_files": [ + "tools/EnterPlanModeTool/constants.ts:1-1", + "tools/EnterPlanModeTool/EnterPlanModeTool.ts:1-126", + "tools/EnterPlanModeTool/prompt.ts:1-170", + "tools/EnterPlanModeTool/UI.tsx:1-33" + ] + }, + { + "name": "EnterWorktreeTool", + "category": "feature-gated", + "source_files": [ + "tools/EnterWorktreeTool/constants.ts:1-1", + "tools/EnterWorktreeTool/EnterWorktreeTool.ts:1-127", + "tools/EnterWorktreeTool/prompt.ts:1-30", + "tools/EnterWorktreeTool/UI.tsx:1-20" + ], + "feature_flags": [ + "fn:isWorktreeModeEnabled" + ] + }, + { + "name": "ExitPlanModeTool", + "category": "runtime-leaf", + "source_files": [ + "tools/ExitPlanModeTool/constants.ts:1-2", + "tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:1-493", + "tools/ExitPlanModeTool/prompt.ts:1-29", + "tools/ExitPlanModeTool/UI.tsx:1-82" + ] + }, + { + "name": "ExitWorktreeTool", + "category": "feature-gated", + "source_files": [ + "tools/ExitWorktreeTool/constants.ts:1-1", + "tools/ExitWorktreeTool/ExitWorktreeTool.ts:1-329", + "tools/ExitWorktreeTool/prompt.ts:1-32", + "tools/ExitWorktreeTool/UI.tsx:1-25" + ], + "feature_flags": [ + "fn:isWorktreeModeEnabled" + ] + }, + { + "name": "FileEditTool", + "category": "runtime-leaf", + "source_files": [ + "tools/FileEditTool/constants.ts:1-11", + "tools/FileEditTool/FileEditTool.ts:1-625", + "tools/FileEditTool/prompt.ts:1-28", + "tools/FileEditTool/types.ts:1-85", + "tools/FileEditTool/UI.tsx:1-289", + "tools/FileEditTool/utils.ts:1-775" + ] + }, + { + "name": "FileReadTool", + "category": "runtime-leaf", + "source_files": [ + "tools/FileReadTool/FileReadTool.ts:1-1183", + "tools/FileReadTool/imageProcessor.ts:1-94", + "tools/FileReadTool/limits.ts:1-92", + "tools/FileReadTool/prompt.ts:1-49", + "tools/FileReadTool/UI.tsx:1-185" + ] + }, + { + "name": "FileWriteTool", + "category": "runtime-leaf", + "source_files": [ + "tools/FileWriteTool/FileWriteTool.ts:1-434", + "tools/FileWriteTool/prompt.ts:1-18", + "tools/FileWriteTool/UI.tsx:1-405" + ] + }, + { + "name": "GlobTool", + "category": "feature-gated", + "source_files": [ + "tools/GlobTool/GlobTool.ts:1-198", + "tools/GlobTool/prompt.ts:1-7", + "tools/GlobTool/UI.tsx:1-63" + ], + "notes": "not directly imported by tools.ts (likely indirect)" + }, + { + "name": "GrepTool", + "category": "feature-gated", + "source_files": [ + "tools/GrepTool/GrepTool.ts:1-577", + "tools/GrepTool/prompt.ts:1-18", + "tools/GrepTool/UI.tsx:1-201" + ], + "notes": "not directly imported by tools.ts (likely indirect)" + }, + { + "name": "LSPTool", + "category": "feature-gated", + "source_files": [ + "tools/LSPTool/formatters.ts:1-592", + "tools/LSPTool/LSPTool.ts:1-860", + "tools/LSPTool/prompt.ts:1-21", + "tools/LSPTool/schemas.ts:1-215", + "tools/LSPTool/symbolContext.ts:1-90", + "tools/LSPTool/UI.tsx:1-228" + ], + "feature_flags": [ + "env:ENABLE_LSP_TOOL" + ] + }, + { + "name": "ListMcpResourcesTool", + "category": "runtime-leaf", + "source_files": [ + "tools/ListMcpResourcesTool/ListMcpResourcesTool.ts:1-123", + "tools/ListMcpResourcesTool/prompt.ts:1-20", + "tools/ListMcpResourcesTool/UI.tsx:1-29" + ] + }, + { + "name": "MCPTool", + "category": "feature-gated", + "source_files": [ + "tools/MCPTool/classifyForCollapse.ts:1-604", + "tools/MCPTool/MCPTool.ts:1-77", + "tools/MCPTool/prompt.ts:1-3", + "tools/MCPTool/UI.tsx:1-403" + ], + "notes": "not directly imported by tools.ts (likely indirect)" + }, + { + "name": "McpAuthTool", + "category": "feature-gated", + "source_files": [ + "tools/McpAuthTool/McpAuthTool.ts:1-215" + ], + "notes": "not directly imported by tools.ts (likely indirect)" + }, + { + "name": "NotebookEditTool", + "category": "runtime-leaf", + "source_files": [ + "tools/NotebookEditTool/constants.ts:1-2", + "tools/NotebookEditTool/NotebookEditTool.ts:1-490", + "tools/NotebookEditTool/prompt.ts:1-3", + "tools/NotebookEditTool/UI.tsx:1-93" + ] + }, + { + "name": "PowerShellTool", + "category": "feature-gated", + "source_files": [ + "tools/PowerShellTool/clmTypes.ts:1-211", + "tools/PowerShellTool/commandSemantics.ts:1-142", + "tools/PowerShellTool/commonParameters.ts:1-30", + "tools/PowerShellTool/destructiveCommandWarning.ts:1-109", + "tools/PowerShellTool/gitSafety.ts:1-176", + "tools/PowerShellTool/modeValidation.ts:1-404", + "tools/PowerShellTool/pathValidation.ts:1-2049", + "tools/PowerShellTool/powershellPermissions.ts:1-1648", + "tools/PowerShellTool/powershellSecurity.ts:1-1090", + "tools/PowerShellTool/PowerShellTool.tsx:1-1001", + "tools/PowerShellTool/prompt.ts:1-145", + "tools/PowerShellTool/readOnlyValidation.ts:1-1823", + "tools/PowerShellTool/toolName.ts:1-2", + "tools/PowerShellTool/UI.tsx:1-131" + ], + "feature_flags": [ + "fn:isPowerShellToolEnabled", + "fn:getPowerShellTool" + ] + }, + { + "name": "REPLTool", + "category": "feature-gated", + "source_files": [ + "tools/REPLTool/constants.ts:1-46", + "tools/REPLTool/primitiveTools.ts:1-39" + ], + "feature_flags": [ + "env:USER_TYPE" + ] + }, + { + "name": "ReadMcpResourceTool", + "category": "runtime-leaf", + "source_files": [ + "tools/ReadMcpResourceTool/prompt.ts:1-16", + "tools/ReadMcpResourceTool/ReadMcpResourceTool.ts:1-158", + "tools/ReadMcpResourceTool/UI.tsx:1-37" + ] + }, + { + "name": "RemoteTriggerTool", + "category": "feature-gated", + "source_files": [ + "tools/RemoteTriggerTool/prompt.ts:1-15", + "tools/RemoteTriggerTool/RemoteTriggerTool.ts:1-161", + "tools/RemoteTriggerTool/UI.tsx:1-17" + ], + "feature_flags": [ + "AGENT_TRIGGERS_REMOTE" + ] + }, + { + "name": "ScheduleCronTool", + "category": "family", + "source_files": [ + "tools/ScheduleCronTool/CronCreateTool.ts:1-157", + "tools/ScheduleCronTool/CronDeleteTool.ts:1-95", + "tools/ScheduleCronTool/CronListTool.ts:1-97", + "tools/ScheduleCronTool/prompt.ts:1-135", + "tools/ScheduleCronTool/UI.tsx:1-60" + ], + "feature_flags": [ + "AGENT_TRIGGERS" + ], + "notes": "family of 3 leaf tools" + }, + { + "name": "CronCreateTool", + "category": "feature-gated", + "source_files": [ + "tools/ScheduleCronTool/CronCreateTool.ts:1-157" + ], + "feature_flags": [ + "AGENT_TRIGGERS" + ], + "notes": "leaf of ScheduleCronTool" + }, + { + "name": "CronDeleteTool", + "category": "feature-gated", + "source_files": [ + "tools/ScheduleCronTool/CronDeleteTool.ts:1-95" + ], + "feature_flags": [ + "AGENT_TRIGGERS" + ], + "notes": "leaf of ScheduleCronTool" + }, + { + "name": "CronListTool", + "category": "feature-gated", + "source_files": [ + "tools/ScheduleCronTool/CronListTool.ts:1-97" + ], + "feature_flags": [ + "AGENT_TRIGGERS" + ], + "notes": "leaf of ScheduleCronTool" + }, + { + "name": "SendMessageTool", + "category": "runtime-leaf", + "source_files": [ + "tools/SendMessageTool/constants.ts:1-1", + "tools/SendMessageTool/prompt.ts:1-49", + "tools/SendMessageTool/SendMessageTool.ts:1-917", + "tools/SendMessageTool/UI.tsx:1-31" + ] + }, + { + "name": "SkillTool", + "category": "runtime-leaf", + "source_files": [ + "tools/SkillTool/constants.ts:1-1", + "tools/SkillTool/prompt.ts:1-241", + "tools/SkillTool/SkillTool.ts:1-1108", + "tools/SkillTool/UI.tsx:1-128" + ] + }, + { + "name": "SleepTool", + "category": "feature-gated", + "source_files": [ + "tools/SleepTool/prompt.ts:1-17" + ], + "feature_flags": [ + "PROACTIVE", + "KAIROS" + ] + }, + { + "name": "SyntheticOutputTool", + "category": "feature-gated", + "source_files": [ + "tools/SyntheticOutputTool/SyntheticOutputTool.ts:1-163" + ], + "notes": "not directly imported by tools.ts (likely indirect)" + }, + { + "name": "TaskCreateTool", + "category": "feature-gated", + "source_files": [ + "tools/TaskCreateTool/constants.ts:1-1", + "tools/TaskCreateTool/prompt.ts:1-56", + "tools/TaskCreateTool/TaskCreateTool.ts:1-138" + ], + "feature_flags": [ + "fn:isTodoV2Enabled" + ] + }, + { + "name": "TaskGetTool", + "category": "feature-gated", + "source_files": [ + "tools/TaskGetTool/constants.ts:1-1", + "tools/TaskGetTool/prompt.ts:1-24", + "tools/TaskGetTool/TaskGetTool.ts:1-128" + ], + "feature_flags": [ + "fn:isTodoV2Enabled" + ] + }, + { + "name": "TaskListTool", + "category": "feature-gated", + "source_files": [ + "tools/TaskListTool/constants.ts:1-1", + "tools/TaskListTool/prompt.ts:1-49", + "tools/TaskListTool/TaskListTool.ts:1-116" + ], + "feature_flags": [ + "fn:isTodoV2Enabled" + ] + }, + { + "name": "TaskOutputTool", + "category": "runtime-leaf", + "source_files": [ + "tools/TaskOutputTool/constants.ts:1-1", + "tools/TaskOutputTool/TaskOutputTool.tsx:1-584" + ] + }, + { + "name": "TaskStopTool", + "category": "runtime-leaf", + "source_files": [ + "tools/TaskStopTool/prompt.ts:1-8", + "tools/TaskStopTool/TaskStopTool.ts:1-131", + "tools/TaskStopTool/UI.tsx:1-41" + ] + }, + { + "name": "TaskUpdateTool", + "category": "feature-gated", + "source_files": [ + "tools/TaskUpdateTool/constants.ts:1-1", + "tools/TaskUpdateTool/prompt.ts:1-77", + "tools/TaskUpdateTool/TaskUpdateTool.ts:1-406" + ], + "feature_flags": [ + "fn:isTodoV2Enabled" + ] + }, + { + "name": "TeamCreateTool", + "category": "feature-gated", + "source_files": [ + "tools/TeamCreateTool/constants.ts:1-1", + "tools/TeamCreateTool/prompt.ts:1-113", + "tools/TeamCreateTool/TeamCreateTool.ts:1-240", + "tools/TeamCreateTool/UI.tsx:1-6" + ], + "feature_flags": [ + "fn:isAgentSwarmsEnabled" + ] + }, + { + "name": "TeamDeleteTool", + "category": "feature-gated", + "source_files": [ + "tools/TeamDeleteTool/constants.ts:1-1", + "tools/TeamDeleteTool/prompt.ts:1-16", + "tools/TeamDeleteTool/TeamDeleteTool.ts:1-139", + "tools/TeamDeleteTool/UI.tsx:1-20" + ], + "feature_flags": [ + "fn:isAgentSwarmsEnabled" + ] + }, + { + "name": "TodoWriteTool", + "category": "runtime-leaf", + "source_files": [ + "tools/TodoWriteTool/constants.ts:1-1", + "tools/TodoWriteTool/prompt.ts:1-184", + "tools/TodoWriteTool/TodoWriteTool.ts:1-115" + ] + }, + { + "name": "ToolSearchTool", + "category": "feature-gated", + "source_files": [ + "tools/ToolSearchTool/constants.ts:1-1", + "tools/ToolSearchTool/prompt.ts:1-121", + "tools/ToolSearchTool/ToolSearchTool.ts:1-471" + ], + "notes": "not directly imported by tools.ts (likely indirect)" + }, + { + "name": "WebFetchTool", + "category": "runtime-leaf", + "source_files": [ + "tools/WebFetchTool/preapproved.ts:1-166", + "tools/WebFetchTool/prompt.ts:1-46", + "tools/WebFetchTool/UI.tsx:1-72", + "tools/WebFetchTool/utils.ts:1-530", + "tools/WebFetchTool/WebFetchTool.ts:1-318" + ] + }, + { + "name": "WebSearchTool", + "category": "runtime-leaf", + "source_files": [ + "tools/WebSearchTool/prompt.ts:1-34", + "tools/WebSearchTool/UI.tsx:1-101", + "tools/WebSearchTool/WebSearchTool.ts:1-435" + ] + } + ] +} diff --git a/docs/appendix/A.md b/docs/appendix/A.md new file mode 100644 index 0000000..dd99582 --- /dev/null +++ b/docs/appendix/A.md @@ -0,0 +1,53 @@ +# 附录 A · 工具速查表 + +> source_commit: `290fdc9481a70612bc5823aa4ed225c52c52aad3` +> generated_at: `2026-05-21T17:10:03.796Z` +> 共 43 项(family=1 / runtime-leaf=19 / feature-gated=23) + +本表由 `scripts/gen-tool-table.ts` 扫描 CLI 源码 `tools/` 目录 + `tools.ts` 注册图(含 `getAllBaseTools()` 内联 gate)生成;正文 §C10 引用此表的 leaf 数即可,不要在正文中裸写工具数量。 + +| name | category | feature_flags | source_files | +|---|---|---|---| +| `AgentTool` | runtime-leaf | — | tools/AgentTool/agentColorManager.ts:1-66, tools/AgentTool/agentDisplay.ts:1-104, tools/AgentTool/agentMemory.ts:1-177 …(+17) | +| `AskUserQuestionTool` | runtime-leaf | — | tools/AskUserQuestionTool/AskUserQuestionTool.tsx:1-266, tools/AskUserQuestionTool/prompt.ts:1-44 | +| `BashTool` | runtime-leaf | — | tools/BashTool/bashCommandHelpers.ts:1-265, tools/BashTool/bashPermissions.ts:1-2621, tools/BashTool/bashSecurity.ts:1-2592 …(+15) | +| `BriefTool` | runtime-leaf | — | tools/BriefTool/attachments.ts:1-110, tools/BriefTool/BriefTool.ts:1-204, tools/BriefTool/prompt.ts:1-22 …(+2) | +| `ConfigTool` | feature-gated | env:USER_TYPE | tools/ConfigTool/ConfigTool.ts:1-467, tools/ConfigTool/constants.ts:1-1, tools/ConfigTool/prompt.ts:1-93 …(+2) | +| `EnterPlanModeTool` | runtime-leaf | — | tools/EnterPlanModeTool/constants.ts:1-1, tools/EnterPlanModeTool/EnterPlanModeTool.ts:1-126, tools/EnterPlanModeTool/prompt.ts:1-170 …(+1) | +| `EnterWorktreeTool` | feature-gated | fn:isWorktreeModeEnabled | tools/EnterWorktreeTool/constants.ts:1-1, tools/EnterWorktreeTool/EnterWorktreeTool.ts:1-127, tools/EnterWorktreeTool/prompt.ts:1-30 …(+1) | +| `ExitPlanModeTool` | runtime-leaf | — | tools/ExitPlanModeTool/constants.ts:1-2, tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:1-493, tools/ExitPlanModeTool/prompt.ts:1-29 …(+1) | +| `ExitWorktreeTool` | feature-gated | fn:isWorktreeModeEnabled | tools/ExitWorktreeTool/constants.ts:1-1, tools/ExitWorktreeTool/ExitWorktreeTool.ts:1-329, tools/ExitWorktreeTool/prompt.ts:1-32 …(+1) | +| `FileEditTool` | runtime-leaf | — | tools/FileEditTool/constants.ts:1-11, tools/FileEditTool/FileEditTool.ts:1-625, tools/FileEditTool/prompt.ts:1-28 …(+3) | +| `FileReadTool` | runtime-leaf | — | tools/FileReadTool/FileReadTool.ts:1-1183, tools/FileReadTool/imageProcessor.ts:1-94, tools/FileReadTool/limits.ts:1-92 …(+2) | +| `FileWriteTool` | runtime-leaf | — | tools/FileWriteTool/FileWriteTool.ts:1-434, tools/FileWriteTool/prompt.ts:1-18, tools/FileWriteTool/UI.tsx:1-405 | +| `GlobTool` | feature-gated | — | tools/GlobTool/GlobTool.ts:1-198, tools/GlobTool/prompt.ts:1-7, tools/GlobTool/UI.tsx:1-63 | +| `GrepTool` | feature-gated | — | tools/GrepTool/GrepTool.ts:1-577, tools/GrepTool/prompt.ts:1-18, tools/GrepTool/UI.tsx:1-201 | +| `LSPTool` | feature-gated | env:ENABLE_LSP_TOOL | tools/LSPTool/formatters.ts:1-592, tools/LSPTool/LSPTool.ts:1-860, tools/LSPTool/prompt.ts:1-21 …(+3) | +| `ListMcpResourcesTool` | runtime-leaf | — | tools/ListMcpResourcesTool/ListMcpResourcesTool.ts:1-123, tools/ListMcpResourcesTool/prompt.ts:1-20, tools/ListMcpResourcesTool/UI.tsx:1-29 | +| `MCPTool` | feature-gated | — | tools/MCPTool/classifyForCollapse.ts:1-604, tools/MCPTool/MCPTool.ts:1-77, tools/MCPTool/prompt.ts:1-3 …(+1) | +| `McpAuthTool` | feature-gated | — | tools/McpAuthTool/McpAuthTool.ts:1-215 | +| `NotebookEditTool` | runtime-leaf | — | tools/NotebookEditTool/constants.ts:1-2, tools/NotebookEditTool/NotebookEditTool.ts:1-490, tools/NotebookEditTool/prompt.ts:1-3 …(+1) | +| `PowerShellTool` | feature-gated | fn:isPowerShellToolEnabled, fn:getPowerShellTool | tools/PowerShellTool/clmTypes.ts:1-211, tools/PowerShellTool/commandSemantics.ts:1-142, tools/PowerShellTool/commonParameters.ts:1-30 …(+11) | +| `REPLTool` | feature-gated | env:USER_TYPE | tools/REPLTool/constants.ts:1-46, tools/REPLTool/primitiveTools.ts:1-39 | +| `ReadMcpResourceTool` | runtime-leaf | — | tools/ReadMcpResourceTool/prompt.ts:1-16, tools/ReadMcpResourceTool/ReadMcpResourceTool.ts:1-158, tools/ReadMcpResourceTool/UI.tsx:1-37 | +| `RemoteTriggerTool` | feature-gated | AGENT_TRIGGERS_REMOTE | tools/RemoteTriggerTool/prompt.ts:1-15, tools/RemoteTriggerTool/RemoteTriggerTool.ts:1-161, tools/RemoteTriggerTool/UI.tsx:1-17 | +| `ScheduleCronTool` | family | AGENT_TRIGGERS | tools/ScheduleCronTool/CronCreateTool.ts:1-157, tools/ScheduleCronTool/CronDeleteTool.ts:1-95, tools/ScheduleCronTool/CronListTool.ts:1-97 …(+2) | +| `CronCreateTool` | feature-gated | AGENT_TRIGGERS | tools/ScheduleCronTool/CronCreateTool.ts:1-157 | +| `CronDeleteTool` | feature-gated | AGENT_TRIGGERS | tools/ScheduleCronTool/CronDeleteTool.ts:1-95 | +| `CronListTool` | feature-gated | AGENT_TRIGGERS | tools/ScheduleCronTool/CronListTool.ts:1-97 | +| `SendMessageTool` | runtime-leaf | — | tools/SendMessageTool/constants.ts:1-1, tools/SendMessageTool/prompt.ts:1-49, tools/SendMessageTool/SendMessageTool.ts:1-917 …(+1) | +| `SkillTool` | runtime-leaf | — | tools/SkillTool/constants.ts:1-1, tools/SkillTool/prompt.ts:1-241, tools/SkillTool/SkillTool.ts:1-1108 …(+1) | +| `SleepTool` | feature-gated | PROACTIVE, KAIROS | tools/SleepTool/prompt.ts:1-17 | +| `SyntheticOutputTool` | feature-gated | — | tools/SyntheticOutputTool/SyntheticOutputTool.ts:1-163 | +| `TaskCreateTool` | feature-gated | fn:isTodoV2Enabled | tools/TaskCreateTool/constants.ts:1-1, tools/TaskCreateTool/prompt.ts:1-56, tools/TaskCreateTool/TaskCreateTool.ts:1-138 | +| `TaskGetTool` | feature-gated | fn:isTodoV2Enabled | tools/TaskGetTool/constants.ts:1-1, tools/TaskGetTool/prompt.ts:1-24, tools/TaskGetTool/TaskGetTool.ts:1-128 | +| `TaskListTool` | feature-gated | fn:isTodoV2Enabled | tools/TaskListTool/constants.ts:1-1, tools/TaskListTool/prompt.ts:1-49, tools/TaskListTool/TaskListTool.ts:1-116 | +| `TaskOutputTool` | runtime-leaf | — | tools/TaskOutputTool/constants.ts:1-1, tools/TaskOutputTool/TaskOutputTool.tsx:1-584 | +| `TaskStopTool` | runtime-leaf | — | tools/TaskStopTool/prompt.ts:1-8, tools/TaskStopTool/TaskStopTool.ts:1-131, tools/TaskStopTool/UI.tsx:1-41 | +| `TaskUpdateTool` | feature-gated | fn:isTodoV2Enabled | tools/TaskUpdateTool/constants.ts:1-1, tools/TaskUpdateTool/prompt.ts:1-77, tools/TaskUpdateTool/TaskUpdateTool.ts:1-406 | +| `TeamCreateTool` | feature-gated | fn:isAgentSwarmsEnabled | tools/TeamCreateTool/constants.ts:1-1, tools/TeamCreateTool/prompt.ts:1-113, tools/TeamCreateTool/TeamCreateTool.ts:1-240 …(+1) | +| `TeamDeleteTool` | feature-gated | fn:isAgentSwarmsEnabled | tools/TeamDeleteTool/constants.ts:1-1, tools/TeamDeleteTool/prompt.ts:1-16, tools/TeamDeleteTool/TeamDeleteTool.ts:1-139 …(+1) | +| `TodoWriteTool` | runtime-leaf | — | tools/TodoWriteTool/constants.ts:1-1, tools/TodoWriteTool/prompt.ts:1-184, tools/TodoWriteTool/TodoWriteTool.ts:1-115 | +| `ToolSearchTool` | feature-gated | — | tools/ToolSearchTool/constants.ts:1-1, tools/ToolSearchTool/prompt.ts:1-121, tools/ToolSearchTool/ToolSearchTool.ts:1-471 | +| `WebFetchTool` | runtime-leaf | — | tools/WebFetchTool/preapproved.ts:1-166, tools/WebFetchTool/prompt.ts:1-46, tools/WebFetchTool/UI.tsx:1-72 …(+2) | +| `WebSearchTool` | runtime-leaf | — | tools/WebSearchTool/prompt.ts:1-34, tools/WebSearchTool/UI.tsx:1-101, tools/WebSearchTool/WebSearchTool.ts:1-435 | diff --git a/docs/appendix/B.manifest.json b/docs/appendix/B.manifest.json new file mode 100644 index 0000000..44a75ae --- /dev/null +++ b/docs/appendix/B.manifest.json @@ -0,0 +1,902 @@ +{ + "generated_at": "2026-05-21T17:10:03.113Z", + "source_commit": "290fdc9481a70612bc5823aa4ed225c52c52aad3", + "appendix": "B", + "items": [ + { + "name": "bridge", + "category": "top-level-dir", + "source_files": [ + "commands/bridge/bridge.tsx:1-509", + "commands/bridge/index.ts:1-26" + ] + }, + { + "name": "remote-setup", + "category": "top-level-dir", + "source_files": [ + "commands/remote-setup/api.ts:1-182", + "commands/remote-setup/index.ts:1-20", + "commands/remote-setup/remote-setup.tsx:1-187" + ] + }, + { + "name": "review", + "category": "top-level-dir", + "source_files": [ + "commands/review/UltrareviewOverageDialog.tsx:1-96", + "commands/review/reviewRemote.ts:1-316", + "commands/review/ultrareviewCommand.tsx:1-58", + "commands/review/ultrareviewEnabled.ts:1-14" + ] + }, + { + "name": "voice", + "category": "top-level-dir", + "source_files": [ + "commands/voice/index.ts:1-20", + "commands/voice/voice.ts:1-150" + ] + }, + { + "name": "add-dir", + "category": "runtime-cmd", + "source_files": [ + "commands/add-dir/add-dir.tsx:1-126", + "commands/add-dir/index.ts:1-11", + "commands/add-dir/validation.ts:1-110" + ], + "notes": "imported by commands.ts" + }, + { + "name": "agents", + "category": "runtime-cmd", + "source_files": [ + "commands/agents/agents.tsx:1-12", + "commands/agents/index.ts:1-10" + ], + "notes": "imported by commands.ts" + }, + { + "name": "ant-trace", + "category": "runtime-cmd", + "source_files": [ + "commands/ant-trace/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "autofix-pr", + "category": "runtime-cmd", + "source_files": [ + "commands/autofix-pr/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "backfill-sessions", + "category": "runtime-cmd", + "source_files": [ + "commands/backfill-sessions/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "branch", + "category": "runtime-cmd", + "source_files": [ + "commands/branch/branch.ts:1-296", + "commands/branch/index.ts:1-14" + ], + "notes": "imported by commands.ts" + }, + { + "name": "break-cache", + "category": "runtime-cmd", + "source_files": [ + "commands/break-cache/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "btw", + "category": "runtime-cmd", + "source_files": [ + "commands/btw/btw.tsx:1-243", + "commands/btw/index.ts:1-13" + ], + "notes": "imported by commands.ts" + }, + { + "name": "bughunter", + "category": "runtime-cmd", + "source_files": [ + "commands/bughunter/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "chrome", + "category": "runtime-cmd", + "source_files": [ + "commands/chrome/chrome.tsx:1-285", + "commands/chrome/index.ts:1-13" + ], + "notes": "imported by commands.ts" + }, + { + "name": "clear", + "category": "runtime-cmd", + "source_files": [ + "commands/clear/caches.ts:1-144", + "commands/clear/clear.ts:1-7", + "commands/clear/conversation.ts:1-251", + "commands/clear/index.ts:1-19" + ], + "notes": "imported by commands.ts" + }, + { + "name": "color", + "category": "runtime-cmd", + "source_files": [ + "commands/color/color.ts:1-93", + "commands/color/index.ts:1-16" + ], + "notes": "imported by commands.ts" + }, + { + "name": "compact", + "category": "runtime-cmd", + "source_files": [ + "commands/compact/compact.ts:1-287", + "commands/compact/index.ts:1-15" + ], + "notes": "imported by commands.ts" + }, + { + "name": "config", + "category": "runtime-cmd", + "source_files": [ + "commands/config/config.tsx:1-7", + "commands/config/index.ts:1-11" + ], + "notes": "imported by commands.ts" + }, + { + "name": "context", + "category": "runtime-cmd", + "source_files": [ + "commands/context/context-noninteractive.ts:1-325", + "commands/context/context.tsx:1-64", + "commands/context/index.ts:1-24" + ], + "notes": "imported by commands.ts" + }, + { + "name": "copy", + "category": "runtime-cmd", + "source_files": [ + "commands/copy/copy.tsx:1-371", + "commands/copy/index.ts:1-15" + ], + "notes": "imported by commands.ts" + }, + { + "name": "cost", + "category": "runtime-cmd", + "source_files": [ + "commands/cost/cost.ts:1-24", + "commands/cost/index.ts:1-23" + ], + "notes": "imported by commands.ts" + }, + { + "name": "ctx_viz", + "category": "runtime-cmd", + "source_files": [ + "commands/ctx_viz/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "debug-tool-call", + "category": "runtime-cmd", + "source_files": [ + "commands/debug-tool-call/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "desktop", + "category": "runtime-cmd", + "source_files": [ + "commands/desktop/desktop.tsx:1-9", + "commands/desktop/index.ts:1-26" + ], + "notes": "imported by commands.ts" + }, + { + "name": "diff", + "category": "runtime-cmd", + "source_files": [ + "commands/diff/diff.tsx:1-9", + "commands/diff/index.ts:1-8" + ], + "notes": "imported by commands.ts" + }, + { + "name": "doctor", + "category": "runtime-cmd", + "source_files": [ + "commands/doctor/doctor.tsx:1-7", + "commands/doctor/index.ts:1-12" + ], + "notes": "imported by commands.ts" + }, + { + "name": "effort", + "category": "runtime-cmd", + "source_files": [ + "commands/effort/effort.tsx:1-183", + "commands/effort/index.ts:1-13" + ], + "notes": "imported by commands.ts" + }, + { + "name": "env", + "category": "runtime-cmd", + "source_files": [ + "commands/env/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "exit", + "category": "runtime-cmd", + "source_files": [ + "commands/exit/exit.tsx:1-33", + "commands/exit/index.ts:1-12" + ], + "notes": "imported by commands.ts" + }, + { + "name": "export", + "category": "runtime-cmd", + "source_files": [ + "commands/export/export.tsx:1-91", + "commands/export/index.ts:1-11" + ], + "notes": "imported by commands.ts" + }, + { + "name": "extra-usage", + "category": "runtime-cmd", + "source_files": [ + "commands/extra-usage/extra-usage-core.ts:1-118", + "commands/extra-usage/extra-usage-noninteractive.ts:1-16", + "commands/extra-usage/extra-usage.tsx:1-17", + "commands/extra-usage/index.ts:1-31" + ], + "notes": "imported by commands.ts" + }, + { + "name": "fast", + "category": "runtime-cmd", + "source_files": [ + "commands/fast/fast.tsx:1-269", + "commands/fast/index.ts:1-26" + ], + "notes": "imported by commands.ts" + }, + { + "name": "feedback", + "category": "runtime-cmd", + "source_files": [ + "commands/feedback/feedback.tsx:1-25", + "commands/feedback/index.ts:1-26" + ], + "notes": "imported by commands.ts" + }, + { + "name": "files", + "category": "runtime-cmd", + "source_files": [ + "commands/files/files.ts:1-19", + "commands/files/index.ts:1-12" + ], + "notes": "imported by commands.ts" + }, + { + "name": "good-claude", + "category": "runtime-cmd", + "source_files": [ + "commands/good-claude/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "heapdump", + "category": "runtime-cmd", + "source_files": [ + "commands/heapdump/heapdump.ts:1-17", + "commands/heapdump/index.ts:1-12" + ], + "notes": "imported by commands.ts" + }, + { + "name": "help", + "category": "runtime-cmd", + "source_files": [ + "commands/help/help.tsx:1-11", + "commands/help/index.ts:1-10" + ], + "notes": "imported by commands.ts" + }, + { + "name": "hooks", + "category": "runtime-cmd", + "source_files": [ + "commands/hooks/hooks.tsx:1-13", + "commands/hooks/index.ts:1-11" + ], + "notes": "imported by commands.ts" + }, + { + "name": "ide", + "category": "runtime-cmd", + "source_files": [ + "commands/ide/ide.tsx:1-646", + "commands/ide/index.ts:1-11" + ], + "notes": "imported by commands.ts" + }, + { + "name": "install-github-app", + "category": "runtime-cmd", + "source_files": [ + "commands/install-github-app/ApiKeyStep.tsx:1-231", + "commands/install-github-app/CheckExistingSecretStep.tsx:1-190", + "commands/install-github-app/CheckGitHubStep.tsx:1-15", + "commands/install-github-app/ChooseRepoStep.tsx:1-211", + "commands/install-github-app/CreatingStep.tsx:1-65", + "commands/install-github-app/ErrorStep.tsx:1-85", + "commands/install-github-app/ExistingWorkflowStep.tsx:1-103", + "commands/install-github-app/InstallAppStep.tsx:1-94", + "commands/install-github-app/OAuthFlowStep.tsx:1-276", + "commands/install-github-app/SuccessStep.tsx:1-96", + "commands/install-github-app/WarningsStep.tsx:1-73", + "commands/install-github-app/index.ts:1-13", + "commands/install-github-app/install-github-app.tsx:1-587", + "commands/install-github-app/setupGitHubActions.ts:1-325" + ], + "notes": "imported by commands.ts" + }, + { + "name": "install-slack-app", + "category": "runtime-cmd", + "source_files": [ + "commands/install-slack-app/index.ts:1-12", + "commands/install-slack-app/install-slack-app.ts:1-30" + ], + "notes": "imported by commands.ts" + }, + { + "name": "issue", + "category": "runtime-cmd", + "source_files": [ + "commands/issue/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "keybindings", + "category": "runtime-cmd", + "source_files": [ + "commands/keybindings/index.ts:1-13", + "commands/keybindings/keybindings.ts:1-53" + ], + "notes": "imported by commands.ts" + }, + { + "name": "login", + "category": "runtime-cmd", + "source_files": [ + "commands/login/index.ts:1-14", + "commands/login/login.tsx:1-104" + ], + "notes": "imported by commands.ts" + }, + { + "name": "logout", + "category": "runtime-cmd", + "source_files": [ + "commands/logout/index.ts:1-10", + "commands/logout/logout.tsx:1-82" + ], + "notes": "imported by commands.ts" + }, + { + "name": "mcp", + "category": "runtime-cmd", + "source_files": [ + "commands/mcp/addCommand.ts:1-280", + "commands/mcp/index.ts:1-12", + "commands/mcp/mcp.tsx:1-85", + "commands/mcp/xaaIdpCommand.ts:1-266" + ], + "notes": "imported by commands.ts" + }, + { + "name": "memory", + "category": "runtime-cmd", + "source_files": [ + "commands/memory/index.ts:1-10", + "commands/memory/memory.tsx:1-90" + ], + "notes": "imported by commands.ts" + }, + { + "name": "mobile", + "category": "runtime-cmd", + "source_files": [ + "commands/mobile/index.ts:1-11", + "commands/mobile/mobile.tsx:1-274" + ], + "notes": "imported by commands.ts" + }, + { + "name": "mock-limits", + "category": "runtime-cmd", + "source_files": [ + "commands/mock-limits/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "model", + "category": "runtime-cmd", + "source_files": [ + "commands/model/index.ts:1-16", + "commands/model/model.tsx:1-297" + ], + "notes": "imported by commands.ts" + }, + { + "name": "oauth-refresh", + "category": "runtime-cmd", + "source_files": [ + "commands/oauth-refresh/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "onboarding", + "category": "runtime-cmd", + "source_files": [ + "commands/onboarding/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "output-style", + "category": "runtime-cmd", + "source_files": [ + "commands/output-style/index.ts:1-11", + "commands/output-style/output-style.tsx:1-7" + ], + "notes": "imported by commands.ts" + }, + { + "name": "passes", + "category": "runtime-cmd", + "source_files": [ + "commands/passes/index.ts:1-22", + "commands/passes/passes.tsx:1-24" + ], + "notes": "imported by commands.ts" + }, + { + "name": "perf-issue", + "category": "runtime-cmd", + "source_files": [ + "commands/perf-issue/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "permissions", + "category": "runtime-cmd", + "source_files": [ + "commands/permissions/index.ts:1-11", + "commands/permissions/permissions.tsx:1-10" + ], + "notes": "imported by commands.ts" + }, + { + "name": "plan", + "category": "runtime-cmd", + "source_files": [ + "commands/plan/index.ts:1-11", + "commands/plan/plan.tsx:1-122" + ], + "notes": "imported by commands.ts" + }, + { + "name": "plugin", + "category": "runtime-cmd", + "source_files": [ + "commands/plugin/AddMarketplace.tsx:1-162", + "commands/plugin/BrowseMarketplace.tsx:1-802", + "commands/plugin/DiscoverPlugins.tsx:1-781", + "commands/plugin/ManageMarketplaces.tsx:1-838", + "commands/plugin/ManagePlugins.tsx:1-2215", + "commands/plugin/PluginErrors.tsx:1-124", + "commands/plugin/PluginOptionsDialog.tsx:1-357", + "commands/plugin/PluginOptionsFlow.tsx:1-135", + "commands/plugin/PluginSettings.tsx:1-1072", + "commands/plugin/PluginTrustWarning.tsx:1-32", + "commands/plugin/UnifiedInstalledCell.tsx:1-565", + "commands/plugin/ValidatePlugin.tsx:1-98", + "commands/plugin/index.tsx:1-11", + "commands/plugin/parseArgs.ts:1-103", + "commands/plugin/plugin.tsx:1-7", + "commands/plugin/pluginDetailsHelpers.tsx:1-117", + "commands/plugin/usePagination.ts:1-171" + ], + "notes": "imported by commands.ts" + }, + { + "name": "pr_comments", + "category": "runtime-cmd", + "source_files": [ + "commands/pr_comments/index.ts:1-50" + ], + "notes": "imported by commands.ts" + }, + { + "name": "privacy-settings", + "category": "runtime-cmd", + "source_files": [ + "commands/privacy-settings/index.ts:1-14", + "commands/privacy-settings/privacy-settings.tsx:1-58" + ], + "notes": "imported by commands.ts" + }, + { + "name": "rate-limit-options", + "category": "runtime-cmd", + "source_files": [ + "commands/rate-limit-options/index.ts:1-19", + "commands/rate-limit-options/rate-limit-options.tsx:1-210" + ], + "notes": "imported by commands.ts" + }, + { + "name": "release-notes", + "category": "runtime-cmd", + "source_files": [ + "commands/release-notes/index.ts:1-11", + "commands/release-notes/release-notes.ts:1-50" + ], + "notes": "imported by commands.ts" + }, + { + "name": "reload-plugins", + "category": "runtime-cmd", + "source_files": [ + "commands/reload-plugins/index.ts:1-18", + "commands/reload-plugins/reload-plugins.ts:1-61" + ], + "notes": "imported by commands.ts" + }, + { + "name": "remote-env", + "category": "runtime-cmd", + "source_files": [ + "commands/remote-env/index.ts:1-15", + "commands/remote-env/remote-env.tsx:1-7" + ], + "notes": "imported by commands.ts" + }, + { + "name": "rename", + "category": "runtime-cmd", + "source_files": [ + "commands/rename/generateSessionName.ts:1-67", + "commands/rename/index.ts:1-12", + "commands/rename/rename.ts:1-87" + ], + "notes": "imported by commands.ts" + }, + { + "name": "reset-limits", + "category": "runtime-cmd", + "source_files": [ + "commands/reset-limits/index.js:1-4" + ], + "notes": "imported by commands.ts" + }, + { + "name": "resume", + "category": "runtime-cmd", + "source_files": [ + "commands/resume/index.ts:1-12", + "commands/resume/resume.tsx:1-275" + ], + "notes": "imported by commands.ts" + }, + { + "name": "rewind", + "category": "runtime-cmd", + "source_files": [ + "commands/rewind/index.ts:1-13", + "commands/rewind/rewind.ts:1-13" + ], + "notes": "imported by commands.ts" + }, + { + "name": "sandbox-toggle", + "category": "runtime-cmd", + "source_files": [ + "commands/sandbox-toggle/index.ts:1-50", + "commands/sandbox-toggle/sandbox-toggle.tsx:1-83" + ], + "notes": "imported by commands.ts" + }, + { + "name": "session", + "category": "runtime-cmd", + "source_files": [ + "commands/session/index.ts:1-16", + "commands/session/session.tsx:1-140" + ], + "notes": "imported by commands.ts" + }, + { + "name": "share", + "category": "runtime-cmd", + "source_files": [ + "commands/share/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "skills", + "category": "runtime-cmd", + "source_files": [ + "commands/skills/index.ts:1-10", + "commands/skills/skills.tsx:1-8" + ], + "notes": "imported by commands.ts" + }, + { + "name": "stats", + "category": "runtime-cmd", + "source_files": [ + "commands/stats/index.ts:1-10", + "commands/stats/stats.tsx:1-7" + ], + "notes": "imported by commands.ts" + }, + { + "name": "status", + "category": "runtime-cmd", + "source_files": [ + "commands/status/index.ts:1-12", + "commands/status/status.tsx:1-8" + ], + "notes": "imported by commands.ts" + }, + { + "name": "stickers", + "category": "runtime-cmd", + "source_files": [ + "commands/stickers/index.ts:1-11", + "commands/stickers/stickers.ts:1-16" + ], + "notes": "imported by commands.ts" + }, + { + "name": "summary", + "category": "runtime-cmd", + "source_files": [ + "commands/summary/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "tag", + "category": "runtime-cmd", + "source_files": [ + "commands/tag/index.ts:1-12", + "commands/tag/tag.tsx:1-215" + ], + "notes": "imported by commands.ts" + }, + { + "name": "tasks", + "category": "runtime-cmd", + "source_files": [ + "commands/tasks/index.ts:1-11", + "commands/tasks/tasks.tsx:1-8" + ], + "notes": "imported by commands.ts" + }, + { + "name": "teleport", + "category": "runtime-cmd", + "source_files": [ + "commands/teleport/index.js:1-1" + ], + "notes": "imported by commands.ts" + }, + { + "name": "terminalSetup", + "category": "runtime-cmd", + "source_files": [ + "commands/terminalSetup/index.ts:1-23", + "commands/terminalSetup/terminalSetup.tsx:1-531" + ], + "notes": "imported by commands.ts" + }, + { + "name": "theme", + "category": "runtime-cmd", + "source_files": [ + "commands/theme/index.ts:1-10", + "commands/theme/theme.tsx:1-57" + ], + "notes": "imported by commands.ts" + }, + { + "name": "thinkback", + "category": "runtime-cmd", + "source_files": [ + "commands/thinkback/index.ts:1-13", + "commands/thinkback/thinkback.tsx:1-554" + ], + "notes": "imported by commands.ts" + }, + { + "name": "thinkback-play", + "category": "runtime-cmd", + "source_files": [ + "commands/thinkback-play/index.ts:1-17", + "commands/thinkback-play/thinkback-play.ts:1-43" + ], + "notes": "imported by commands.ts" + }, + { + "name": "upgrade", + "category": "runtime-cmd", + "source_files": [ + "commands/upgrade/index.ts:1-16", + "commands/upgrade/upgrade.tsx:1-38" + ], + "notes": "imported by commands.ts" + }, + { + "name": "usage", + "category": "runtime-cmd", + "source_files": [ + "commands/usage/index.ts:1-9", + "commands/usage/usage.tsx:1-7" + ], + "notes": "imported by commands.ts" + }, + { + "name": "vim", + "category": "runtime-cmd", + "source_files": [ + "commands/vim/index.ts:1-11", + "commands/vim/vim.ts:1-38" + ], + "notes": "imported by commands.ts" + }, + { + "name": "advisor", + "category": "top-level-file", + "source_files": [ + "commands/advisor.ts:1-109" + ] + }, + { + "name": "bridge-kick", + "category": "top-level-file", + "source_files": [ + "commands/bridge-kick.ts:1-200" + ] + }, + { + "name": "brief", + "category": "top-level-file", + "source_files": [ + "commands/brief.ts:1-130" + ] + }, + { + "name": "commit", + "category": "top-level-file", + "source_files": [ + "commands/commit.ts:1-92" + ] + }, + { + "name": "commit-push-pr", + "category": "top-level-file", + "source_files": [ + "commands/commit-push-pr.ts:1-158" + ] + }, + { + "name": "createMovedToPluginCommand", + "category": "top-level-file", + "source_files": [ + "commands/createMovedToPluginCommand.ts:1-65" + ] + }, + { + "name": "init", + "category": "top-level-file", + "source_files": [ + "commands/init.ts:1-256" + ] + }, + { + "name": "init-verifiers", + "category": "top-level-file", + "source_files": [ + "commands/init-verifiers.ts:1-262" + ] + }, + { + "name": "insights", + "category": "top-level-file", + "source_files": [ + "commands/insights.ts:1-3200" + ] + }, + { + "name": "install", + "category": "top-level-file", + "source_files": [ + "commands/install.tsx:1-300" + ] + }, + { + "name": "review", + "category": "top-level-file", + "source_files": [ + "commands/review.ts:1-57" + ] + }, + { + "name": "security-review", + "category": "top-level-file", + "source_files": [ + "commands/security-review.ts:1-243" + ] + }, + { + "name": "statusline", + "category": "top-level-file", + "source_files": [ + "commands/statusline.tsx:1-24" + ] + }, + { + "name": "ultraplan", + "category": "top-level-file", + "source_files": [ + "commands/ultraplan.tsx:1-471" + ] + }, + { + "name": "version", + "category": "top-level-file", + "source_files": [ + "commands/version.ts:1-22" + ] + } + ] +} diff --git a/docs/appendix/B.md b/docs/appendix/B.md new file mode 100644 index 0000000..cce7e25 --- /dev/null +++ b/docs/appendix/B.md @@ -0,0 +1,111 @@ +# 附录 B · Commands 速查表 + +> source_commit: `290fdc9481a70612bc5823aa4ed225c52c52aad3` +> generated_at: `2026-05-21T17:10:03.113Z` +> 共 101 项(top-level-dir=4 / runtime-cmd=82 / top-level-file=15) + +由 `scripts/gen-commands-table.ts` 扫描 `commands/` 一级目录、`commands/*.ts` 一级文件,并交叉 `commands.ts` 注册图。正文 §C32 引用本表,不裸写命令数。 + +| name | category | source_files | +|---|---|---| +| `bridge` | top-level-dir | commands/bridge/bridge.tsx:1-509, commands/bridge/index.ts:1-26 | +| `remote-setup` | top-level-dir | commands/remote-setup/api.ts:1-182, commands/remote-setup/index.ts:1-20, commands/remote-setup/remote-setup.tsx:1-187 | +| `review` | top-level-dir | commands/review/UltrareviewOverageDialog.tsx:1-96, commands/review/reviewRemote.ts:1-316, commands/review/ultrareviewCommand.tsx:1-58 …(+1) | +| `voice` | top-level-dir | commands/voice/index.ts:1-20, commands/voice/voice.ts:1-150 | +| `add-dir` | runtime-cmd | commands/add-dir/add-dir.tsx:1-126, commands/add-dir/index.ts:1-11, commands/add-dir/validation.ts:1-110 | +| `agents` | runtime-cmd | commands/agents/agents.tsx:1-12, commands/agents/index.ts:1-10 | +| `ant-trace` | runtime-cmd | commands/ant-trace/index.js:1-1 | +| `autofix-pr` | runtime-cmd | commands/autofix-pr/index.js:1-1 | +| `backfill-sessions` | runtime-cmd | commands/backfill-sessions/index.js:1-1 | +| `branch` | runtime-cmd | commands/branch/branch.ts:1-296, commands/branch/index.ts:1-14 | +| `break-cache` | runtime-cmd | commands/break-cache/index.js:1-1 | +| `btw` | runtime-cmd | commands/btw/btw.tsx:1-243, commands/btw/index.ts:1-13 | +| `bughunter` | runtime-cmd | commands/bughunter/index.js:1-1 | +| `chrome` | runtime-cmd | commands/chrome/chrome.tsx:1-285, commands/chrome/index.ts:1-13 | +| `clear` | runtime-cmd | commands/clear/caches.ts:1-144, commands/clear/clear.ts:1-7, commands/clear/conversation.ts:1-251 …(+1) | +| `color` | runtime-cmd | commands/color/color.ts:1-93, commands/color/index.ts:1-16 | +| `compact` | runtime-cmd | commands/compact/compact.ts:1-287, commands/compact/index.ts:1-15 | +| `config` | runtime-cmd | commands/config/config.tsx:1-7, commands/config/index.ts:1-11 | +| `context` | runtime-cmd | commands/context/context-noninteractive.ts:1-325, commands/context/context.tsx:1-64, commands/context/index.ts:1-24 | +| `copy` | runtime-cmd | commands/copy/copy.tsx:1-371, commands/copy/index.ts:1-15 | +| `cost` | runtime-cmd | commands/cost/cost.ts:1-24, commands/cost/index.ts:1-23 | +| `ctx_viz` | runtime-cmd | commands/ctx_viz/index.js:1-1 | +| `debug-tool-call` | runtime-cmd | commands/debug-tool-call/index.js:1-1 | +| `desktop` | runtime-cmd | commands/desktop/desktop.tsx:1-9, commands/desktop/index.ts:1-26 | +| `diff` | runtime-cmd | commands/diff/diff.tsx:1-9, commands/diff/index.ts:1-8 | +| `doctor` | runtime-cmd | commands/doctor/doctor.tsx:1-7, commands/doctor/index.ts:1-12 | +| `effort` | runtime-cmd | commands/effort/effort.tsx:1-183, commands/effort/index.ts:1-13 | +| `env` | runtime-cmd | commands/env/index.js:1-1 | +| `exit` | runtime-cmd | commands/exit/exit.tsx:1-33, commands/exit/index.ts:1-12 | +| `export` | runtime-cmd | commands/export/export.tsx:1-91, commands/export/index.ts:1-11 | +| `extra-usage` | runtime-cmd | commands/extra-usage/extra-usage-core.ts:1-118, commands/extra-usage/extra-usage-noninteractive.ts:1-16, commands/extra-usage/extra-usage.tsx:1-17 …(+1) | +| `fast` | runtime-cmd | commands/fast/fast.tsx:1-269, commands/fast/index.ts:1-26 | +| `feedback` | runtime-cmd | commands/feedback/feedback.tsx:1-25, commands/feedback/index.ts:1-26 | +| `files` | runtime-cmd | commands/files/files.ts:1-19, commands/files/index.ts:1-12 | +| `good-claude` | runtime-cmd | commands/good-claude/index.js:1-1 | +| `heapdump` | runtime-cmd | commands/heapdump/heapdump.ts:1-17, commands/heapdump/index.ts:1-12 | +| `help` | runtime-cmd | commands/help/help.tsx:1-11, commands/help/index.ts:1-10 | +| `hooks` | runtime-cmd | commands/hooks/hooks.tsx:1-13, commands/hooks/index.ts:1-11 | +| `ide` | runtime-cmd | commands/ide/ide.tsx:1-646, commands/ide/index.ts:1-11 | +| `install-github-app` | runtime-cmd | commands/install-github-app/ApiKeyStep.tsx:1-231, commands/install-github-app/CheckExistingSecretStep.tsx:1-190, commands/install-github-app/CheckGitHubStep.tsx:1-15 …(+11) | +| `install-slack-app` | runtime-cmd | commands/install-slack-app/index.ts:1-12, commands/install-slack-app/install-slack-app.ts:1-30 | +| `issue` | runtime-cmd | commands/issue/index.js:1-1 | +| `keybindings` | runtime-cmd | commands/keybindings/index.ts:1-13, commands/keybindings/keybindings.ts:1-53 | +| `login` | runtime-cmd | commands/login/index.ts:1-14, commands/login/login.tsx:1-104 | +| `logout` | runtime-cmd | commands/logout/index.ts:1-10, commands/logout/logout.tsx:1-82 | +| `mcp` | runtime-cmd | commands/mcp/addCommand.ts:1-280, commands/mcp/index.ts:1-12, commands/mcp/mcp.tsx:1-85 …(+1) | +| `memory` | runtime-cmd | commands/memory/index.ts:1-10, commands/memory/memory.tsx:1-90 | +| `mobile` | runtime-cmd | commands/mobile/index.ts:1-11, commands/mobile/mobile.tsx:1-274 | +| `mock-limits` | runtime-cmd | commands/mock-limits/index.js:1-1 | +| `model` | runtime-cmd | commands/model/index.ts:1-16, commands/model/model.tsx:1-297 | +| `oauth-refresh` | runtime-cmd | commands/oauth-refresh/index.js:1-1 | +| `onboarding` | runtime-cmd | commands/onboarding/index.js:1-1 | +| `output-style` | runtime-cmd | commands/output-style/index.ts:1-11, commands/output-style/output-style.tsx:1-7 | +| `passes` | runtime-cmd | commands/passes/index.ts:1-22, commands/passes/passes.tsx:1-24 | +| `perf-issue` | runtime-cmd | commands/perf-issue/index.js:1-1 | +| `permissions` | runtime-cmd | commands/permissions/index.ts:1-11, commands/permissions/permissions.tsx:1-10 | +| `plan` | runtime-cmd | commands/plan/index.ts:1-11, commands/plan/plan.tsx:1-122 | +| `plugin` | runtime-cmd | commands/plugin/AddMarketplace.tsx:1-162, commands/plugin/BrowseMarketplace.tsx:1-802, commands/plugin/DiscoverPlugins.tsx:1-781 …(+14) | +| `pr_comments` | runtime-cmd | commands/pr_comments/index.ts:1-50 | +| `privacy-settings` | runtime-cmd | commands/privacy-settings/index.ts:1-14, commands/privacy-settings/privacy-settings.tsx:1-58 | +| `rate-limit-options` | runtime-cmd | commands/rate-limit-options/index.ts:1-19, commands/rate-limit-options/rate-limit-options.tsx:1-210 | +| `release-notes` | runtime-cmd | commands/release-notes/index.ts:1-11, commands/release-notes/release-notes.ts:1-50 | +| `reload-plugins` | runtime-cmd | commands/reload-plugins/index.ts:1-18, commands/reload-plugins/reload-plugins.ts:1-61 | +| `remote-env` | runtime-cmd | commands/remote-env/index.ts:1-15, commands/remote-env/remote-env.tsx:1-7 | +| `rename` | runtime-cmd | commands/rename/generateSessionName.ts:1-67, commands/rename/index.ts:1-12, commands/rename/rename.ts:1-87 | +| `reset-limits` | runtime-cmd | commands/reset-limits/index.js:1-4 | +| `resume` | runtime-cmd | commands/resume/index.ts:1-12, commands/resume/resume.tsx:1-275 | +| `rewind` | runtime-cmd | commands/rewind/index.ts:1-13, commands/rewind/rewind.ts:1-13 | +| `sandbox-toggle` | runtime-cmd | commands/sandbox-toggle/index.ts:1-50, commands/sandbox-toggle/sandbox-toggle.tsx:1-83 | +| `session` | runtime-cmd | commands/session/index.ts:1-16, commands/session/session.tsx:1-140 | +| `share` | runtime-cmd | commands/share/index.js:1-1 | +| `skills` | runtime-cmd | commands/skills/index.ts:1-10, commands/skills/skills.tsx:1-8 | +| `stats` | runtime-cmd | commands/stats/index.ts:1-10, commands/stats/stats.tsx:1-7 | +| `status` | runtime-cmd | commands/status/index.ts:1-12, commands/status/status.tsx:1-8 | +| `stickers` | runtime-cmd | commands/stickers/index.ts:1-11, commands/stickers/stickers.ts:1-16 | +| `summary` | runtime-cmd | commands/summary/index.js:1-1 | +| `tag` | runtime-cmd | commands/tag/index.ts:1-12, commands/tag/tag.tsx:1-215 | +| `tasks` | runtime-cmd | commands/tasks/index.ts:1-11, commands/tasks/tasks.tsx:1-8 | +| `teleport` | runtime-cmd | commands/teleport/index.js:1-1 | +| `terminalSetup` | runtime-cmd | commands/terminalSetup/index.ts:1-23, commands/terminalSetup/terminalSetup.tsx:1-531 | +| `theme` | runtime-cmd | commands/theme/index.ts:1-10, commands/theme/theme.tsx:1-57 | +| `thinkback` | runtime-cmd | commands/thinkback/index.ts:1-13, commands/thinkback/thinkback.tsx:1-554 | +| `thinkback-play` | runtime-cmd | commands/thinkback-play/index.ts:1-17, commands/thinkback-play/thinkback-play.ts:1-43 | +| `upgrade` | runtime-cmd | commands/upgrade/index.ts:1-16, commands/upgrade/upgrade.tsx:1-38 | +| `usage` | runtime-cmd | commands/usage/index.ts:1-9, commands/usage/usage.tsx:1-7 | +| `vim` | runtime-cmd | commands/vim/index.ts:1-11, commands/vim/vim.ts:1-38 | +| `advisor` | top-level-file | commands/advisor.ts:1-109 | +| `bridge-kick` | top-level-file | commands/bridge-kick.ts:1-200 | +| `brief` | top-level-file | commands/brief.ts:1-130 | +| `commit` | top-level-file | commands/commit.ts:1-92 | +| `commit-push-pr` | top-level-file | commands/commit-push-pr.ts:1-158 | +| `createMovedToPluginCommand` | top-level-file | commands/createMovedToPluginCommand.ts:1-65 | +| `init` | top-level-file | commands/init.ts:1-256 | +| `init-verifiers` | top-level-file | commands/init-verifiers.ts:1-262 | +| `insights` | top-level-file | commands/insights.ts:1-3200 | +| `install` | top-level-file | commands/install.tsx:1-300 | +| `review` | top-level-file | commands/review.ts:1-57 | +| `security-review` | top-level-file | commands/security-review.ts:1-243 | +| `statusline` | top-level-file | commands/statusline.tsx:1-24 | +| `ultraplan` | top-level-file | commands/ultraplan.tsx:1-471 | +| `version` | top-level-file | commands/version.ts:1-22 | diff --git a/docs/appendix/C.manifest.json b/docs/appendix/C.manifest.json new file mode 100644 index 0000000..a051e5b --- /dev/null +++ b/docs/appendix/C.manifest.json @@ -0,0 +1,255 @@ +{ + "generated_at": "2026-05-21T16:18:51.605Z", + "source_commit": "290fdc9481a70612bc5823aa4ed225c52c52aad3", + "appendix": "C", + "items": [ + { + "name": "PreToolUse", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:26" + ], + "wire_type": "hook_event" + }, + { + "name": "PostToolUse", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:27" + ], + "wire_type": "hook_event" + }, + { + "name": "PostToolUseFailure", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:28" + ], + "wire_type": "hook_event" + }, + { + "name": "Notification", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:29" + ], + "wire_type": "hook_event" + }, + { + "name": "UserPromptSubmit", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:30" + ], + "wire_type": "hook_event" + }, + { + "name": "SessionStart", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:31" + ], + "wire_type": "hook_event" + }, + { + "name": "SessionEnd", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:32" + ], + "wire_type": "hook_event" + }, + { + "name": "Stop", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:33" + ], + "wire_type": "hook_event" + }, + { + "name": "StopFailure", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:34" + ], + "wire_type": "hook_event" + }, + { + "name": "SubagentStart", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:35" + ], + "wire_type": "hook_event" + }, + { + "name": "SubagentStop", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:36" + ], + "wire_type": "hook_event" + }, + { + "name": "PreCompact", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:37" + ], + "wire_type": "hook_event" + }, + { + "name": "PostCompact", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:38" + ], + "wire_type": "hook_event" + }, + { + "name": "PermissionRequest", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:39" + ], + "wire_type": "hook_event" + }, + { + "name": "PermissionDenied", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:40" + ], + "wire_type": "hook_event" + }, + { + "name": "Setup", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:41" + ], + "wire_type": "hook_event" + }, + { + "name": "TeammateIdle", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:42" + ], + "wire_type": "hook_event" + }, + { + "name": "TaskCreated", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:43" + ], + "wire_type": "hook_event" + }, + { + "name": "TaskCompleted", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:44" + ], + "wire_type": "hook_event" + }, + { + "name": "Elicitation", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:45" + ], + "wire_type": "hook_event" + }, + { + "name": "ElicitationResult", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:46" + ], + "wire_type": "hook_event" + }, + { + "name": "ConfigChange", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:47" + ], + "wire_type": "hook_event" + }, + { + "name": "WorktreeCreate", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:48" + ], + "wire_type": "hook_event" + }, + { + "name": "WorktreeRemove", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:49" + ], + "wire_type": "hook_event" + }, + { + "name": "InstructionsLoaded", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:50" + ], + "wire_type": "hook_event" + }, + { + "name": "CwdChanged", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:51" + ], + "wire_type": "hook_event" + }, + { + "name": "FileChanged", + "category": "event", + "source_files": [ + "entrypoints/sdk/coreTypes.ts:52" + ], + "wire_type": "hook_event" + }, + { + "name": "agent", + "category": "command-kind", + "source_files": [ + "schemas/hooks.ts" + ], + "wire_type": "hook_command" + }, + { + "name": "command", + "category": "command-kind", + "source_files": [ + "schemas/hooks.ts" + ], + "wire_type": "hook_command" + }, + { + "name": "http", + "category": "command-kind", + "source_files": [ + "schemas/hooks.ts" + ], + "wire_type": "hook_command" + }, + { + "name": "prompt", + "category": "command-kind", + "source_files": [ + "schemas/hooks.ts" + ], + "wire_type": "hook_command" + } + ] +} diff --git a/docs/appendix/C.md b/docs/appendix/C.md new file mode 100644 index 0000000..d55d82a --- /dev/null +++ b/docs/appendix/C.md @@ -0,0 +1,48 @@ +# 附录 C · Hooks 事件表 + +> source_commit: `290fdc9481a70612bc5823aa4ed225c52c52aad3` +> generated_at: `2026-05-21T16:18:51.605Z` +> 共 27 个 HOOK_EVENTS + 4 种 hook command + +由 `scripts/gen-hooks-table.ts` 扫描 `entrypoints/sdk/coreTypes.ts` (HOOK_EVENTS 字面量) 与 `schemas/hooks.ts`(命令 kind 判别)。正文 §C20 不裸写"27",引用本表。 + +## C.1 HOOK_EVENTS + +| # | name | source | +|---|---|---| +| 1 | `PreToolUse` | entrypoints/sdk/coreTypes.ts:26 | +| 2 | `PostToolUse` | entrypoints/sdk/coreTypes.ts:27 | +| 3 | `PostToolUseFailure` | entrypoints/sdk/coreTypes.ts:28 | +| 4 | `Notification` | entrypoints/sdk/coreTypes.ts:29 | +| 5 | `UserPromptSubmit` | entrypoints/sdk/coreTypes.ts:30 | +| 6 | `SessionStart` | entrypoints/sdk/coreTypes.ts:31 | +| 7 | `SessionEnd` | entrypoints/sdk/coreTypes.ts:32 | +| 8 | `Stop` | entrypoints/sdk/coreTypes.ts:33 | +| 9 | `StopFailure` | entrypoints/sdk/coreTypes.ts:34 | +| 10 | `SubagentStart` | entrypoints/sdk/coreTypes.ts:35 | +| 11 | `SubagentStop` | entrypoints/sdk/coreTypes.ts:36 | +| 12 | `PreCompact` | entrypoints/sdk/coreTypes.ts:37 | +| 13 | `PostCompact` | entrypoints/sdk/coreTypes.ts:38 | +| 14 | `PermissionRequest` | entrypoints/sdk/coreTypes.ts:39 | +| 15 | `PermissionDenied` | entrypoints/sdk/coreTypes.ts:40 | +| 16 | `Setup` | entrypoints/sdk/coreTypes.ts:41 | +| 17 | `TeammateIdle` | entrypoints/sdk/coreTypes.ts:42 | +| 18 | `TaskCreated` | entrypoints/sdk/coreTypes.ts:43 | +| 19 | `TaskCompleted` | entrypoints/sdk/coreTypes.ts:44 | +| 20 | `Elicitation` | entrypoints/sdk/coreTypes.ts:45 | +| 21 | `ElicitationResult` | entrypoints/sdk/coreTypes.ts:46 | +| 22 | `ConfigChange` | entrypoints/sdk/coreTypes.ts:47 | +| 23 | `WorktreeCreate` | entrypoints/sdk/coreTypes.ts:48 | +| 24 | `WorktreeRemove` | entrypoints/sdk/coreTypes.ts:49 | +| 25 | `InstructionsLoaded` | entrypoints/sdk/coreTypes.ts:50 | +| 26 | `CwdChanged` | entrypoints/sdk/coreTypes.ts:51 | +| 27 | `FileChanged` | entrypoints/sdk/coreTypes.ts:52 | + +## C.2 Hook command kinds + +| name | source | +|---|---| +| `agent` | schemas/hooks.ts | +| `command` | schemas/hooks.ts | +| `http` | schemas/hooks.ts | +| `prompt` | schemas/hooks.ts | diff --git a/docs/appendix/D.manifest.json b/docs/appendix/D.manifest.json new file mode 100644 index 0000000..1b823e3 --- /dev/null +++ b/docs/appendix/D.manifest.json @@ -0,0 +1,55 @@ +{ + "generated_at": "2026-05-21T16:18:51.893Z", + "source_commit": "290fdc9481a70612bc5823aa4ed225c52c52aad3", + "appendix": "D", + "items": [ + { + "name": "CLAUDE_CODE_GUIDE_AGENT_TYPE", + "category": "built-in", + "source_files": [ + "tools/AgentTool/built-in/claudeCodeGuideAgent.ts:100" + ], + "notes": "model=haiku" + }, + { + "name": "Explore", + "category": "built-in", + "source_files": [ + "tools/AgentTool/built-in/exploreAgent.ts:66" + ], + "notes": "model=process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku'" + }, + { + "name": "general-purpose", + "category": "built-in", + "source_files": [ + "tools/AgentTool/built-in/generalPurposeAgent.ts:27" + ], + "notes": "tools=['*']" + }, + { + "name": "Plan", + "category": "built-in", + "source_files": [ + "tools/AgentTool/built-in/planAgent.ts:75" + ], + "notes": "model=inherit" + }, + { + "name": "statusline-setup", + "category": "built-in", + "source_files": [ + "tools/AgentTool/built-in/statuslineSetup.ts:136" + ], + "notes": "model=sonnet; tools=['Read', 'Edit']" + }, + { + "name": "verification", + "category": "built-in", + "source_files": [ + "tools/AgentTool/built-in/verificationAgent.ts:136" + ], + "notes": "model=inherit" + } + ] +} diff --git a/docs/appendix/D.md b/docs/appendix/D.md new file mode 100644 index 0000000..ef40a50 --- /dev/null +++ b/docs/appendix/D.md @@ -0,0 +1,16 @@ +# 附录 D · 内置 Agent 速查表 + +> source_commit: `290fdc9481a70612bc5823aa4ed225c52c52aad3` +> generated_at: `2026-05-21T16:18:51.893Z` +> 共 6 个源码定义的 built-in agent + +本表只列「源码定义」的 Agent;运行时是否启用受 feature flag / entrypoint / coordinator 影响——见 `gates` 列。正文 §C15 必须声明这一两段式(spec §7.5)。 + +| agentType (or symbol) | source_file | gates (feature/entry/coord) | notes | +|---|---|---|---| +| `CLAUDE_CODE_GUIDE_AGENT_TYPE` | tools/AgentTool/built-in/claudeCodeGuideAgent.ts:100 | — | model=haiku | +| `Explore` | tools/AgentTool/built-in/exploreAgent.ts:66 | — | model=process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku' | +| `general-purpose` | tools/AgentTool/built-in/generalPurposeAgent.ts:27 | — | tools=['*'] | +| `Plan` | tools/AgentTool/built-in/planAgent.ts:75 | — | model=inherit | +| `statusline-setup` | tools/AgentTool/built-in/statuslineSetup.ts:136 | — | model=sonnet; tools=['Read', 'Edit'] | +| `verification` | tools/AgentTool/built-in/verificationAgent.ts:136 | — | model=inherit | diff --git a/docs/appendix/E.manifest.json b/docs/appendix/E.manifest.json new file mode 100644 index 0000000..55621f4 --- /dev/null +++ b/docs/appendix/E.manifest.json @@ -0,0 +1,77 @@ +{ + "generated_at": "2026-05-21T16:18:20.023Z", + "source_commit": "290fdc9481a70612bc5823aa4ed225c52c52aad3", + "appendix": "E", + "items": [ + { + "name": "local_bash", + "category": "default", + "source_files": [ + "Task.ts:6-13" + ], + "wire_type": "local_bash", + "default_registered": true + }, + { + "name": "local_agent", + "category": "default", + "source_files": [ + "Task.ts:6-13" + ], + "wire_type": "local_agent", + "default_registered": true + }, + { + "name": "remote_agent", + "category": "default", + "source_files": [ + "Task.ts:6-13" + ], + "wire_type": "remote_agent", + "default_registered": true + }, + { + "name": "in_process_teammate", + "category": "in-process-special", + "source_files": [ + "Task.ts:6-13" + ], + "wire_type": "in_process_teammate", + "default_registered": false, + "notes": "wire type only; runs in-process via teammate path, not via getAllTasks()" + }, + { + "name": "local_workflow", + "category": "feature-gated", + "source_files": [ + "Task.ts:6-13" + ], + "wire_type": "local_workflow", + "default_registered": false, + "feature_flags": [ + "WORKFLOW_SCRIPTS" + ] + }, + { + "name": "monitor_mcp", + "category": "feature-gated", + "source_files": [ + "Task.ts:6-13" + ], + "wire_type": "monitor_mcp", + "default_registered": false, + "feature_flags": [ + "MONITOR_TOOL" + ] + }, + { + "name": "dream", + "category": "default", + "source_files": [ + "tasks/DreamTask/DreamTask.ts:1-157" + ], + "wire_type": "dream", + "default_registered": true + } + ] +} diff --git a/docs/appendix/E.md b/docs/appendix/E.md new file mode 100644 index 0000000..3bb8362 --- /dev/null +++ b/docs/appendix/E.md @@ -0,0 +1,17 @@ +# 附录 E · TaskType 谱系 + +> source_commit: `290fdc9481a70612bc5823aa4ed225c52c52aad3` +> generated_at: `2026-05-21T16:18:20.023Z` +> 共 7 个 wire TaskType(default=4 / feature-gated=2 / in-process-special=1) + +由 `scripts/gen-tasktypes-table.ts` 解析 `Task.ts` 的 `TaskType` union 字面量与 `tasks.ts` 的 `getAllTasks()` 注册体。正文 §C16 引用本表,禁止裸写 4/2/1。 + +| name | category | feature_flags | default_registered | source_file | notes | +|---|---|---|---|---|---| +| `local_bash` | default | — | true | Task.ts:6-13 | — | +| `local_agent` | default | — | true | Task.ts:6-13 | — | +| `remote_agent` | default | — | true | Task.ts:6-13 | — | +| `in_process_teammate` | in-process-special | — | false | Task.ts:6-13 | wire type only; runs in-process via teammate path, not via getAllTasks() | +| `local_workflow` | feature-gated | WORKFLOW_SCRIPTS | false | Task.ts:6-13 | — | +| `monitor_mcp` | feature-gated | MONITOR_TOOL | false | Task.ts:6-13 | — | +| `dream` | default | — | true | tasks/DreamTask/DreamTask.ts:1-157 | — | diff --git a/docs/appendix/F.manifest.json b/docs/appendix/F.manifest.json new file mode 100644 index 0000000..9b71fa7 --- /dev/null +++ b/docs/appendix/F.manifest.json @@ -0,0 +1,319 @@ +{ + "generated_at": "2026-05-21T17:10:29.258Z", + "source_commit": "290fdc9481a70612bc5823aa4ed225c52c52aad3", + "appendix": "F", + "items": [ + { + "name": "C01", + "category": "chapter", + "source_files": [ + "entrypoints", + "bridge", + "remote", + "coordinator", + "buddy", + "upstreamproxy", + "server", + "migrations", + "native-ts", + "screens", + "outputStyles", + "memdir", + "assistant", + "schemas" + ] + }, + { + "name": "C02", + "category": "chapter", + "source_files": [ + "bootstrap", + "main.tsx", + "replLauncher.tsx", + "dialogLaunchers.tsx", + "interactiveHelpers.tsx" + ] + }, + { + "name": "C03", + "category": "chapter", + "source_files": [ + "services" + ] + }, + { + "name": "C04", + "category": "chapter", + "source_files": [ + "migrations" + ] + }, + { + "name": "C05", + "category": "chapter", + "source_files": [ + "QueryEngine.ts", + "query.ts", + "query" + ] + }, + { + "name": "C06", + "category": "chapter", + "source_files": [ + "constants", + "outputStyles" + ] + }, + { + "name": "C07", + "category": "chapter", + "source_files": [ + "services" + ] + }, + { + "name": "C08", + "category": "chapter", + "source_files": [ + "services" + ] + }, + { + "name": "C09", + "category": "chapter", + "source_files": [ + "commands", + "services" + ] + }, + { + "name": "C10", + "category": "chapter", + "source_files": [ + "Tool.ts", + "tools.ts", + "tools" + ] + }, + { + "name": "C11", + "category": "chapter", + "source_files": [ + "tools" + ] + }, + { + "name": "C12", + "category": "chapter", + "source_files": [ + "tools", + "services" + ] + }, + { + "name": "C13", + "category": "chapter", + "source_files": [ + "tools" + ] + }, + { + "name": "C14", + "category": "chapter", + "source_files": [ + "tools", + "services", + "commands" + ] + }, + { + "name": "C15", + "category": "chapter", + "source_files": [ + "tools", + "services" + ] + }, + { + "name": "C16", + "category": "chapter", + "source_files": [ + "Task.ts", + "tasks.ts", + "tasks", + "tools" + ] + }, + { + "name": "C17", + "category": "chapter", + "source_files": [ + "coordinator", + "tools", + "hooks" + ] + }, + { + "name": "C18", + "category": "chapter", + "source_files": [ + "services", + "tools" + ] + }, + { + "name": "C19", + "category": "chapter", + "source_files": [ + "Tool.ts", + "hooks", + "bridge", + "remote" + ] + }, + { + "name": "C20", + "category": "chapter", + "source_files": [ + "schemas", + "hooks", + "query" + ] + }, + { + "name": "C21", + "category": "chapter", + "source_files": [ + "skills", + "services", + "plugins", + "outputStyles" + ] + }, + { + "name": "C22", + "category": "chapter", + "source_files": [ + "utils", + "constants" + ] + }, + { + "name": "C23", + "category": "chapter", + "source_files": [ + "services", + "cli" + ] + }, + { + "name": "C24", + "category": "chapter", + "source_files": [ + "bridge", + "remote", + "commands" + ] + }, + { + "name": "C25", + "category": "chapter", + "source_files": [ + "server", + "upstreamproxy", + "hooks" + ] + }, + { + "name": "C26", + "category": "chapter", + "source_files": [ + "ink", + "native-ts" + ] + }, + { + "name": "C27", + "category": "chapter", + "source_files": [ + "components" + ] + }, + { + "name": "C28", + "category": "chapter", + "source_files": [ + "keybindings", + "vim", + "voice", + "services", + "hooks", + "commands" + ] + }, + { + "name": "C29", + "category": "chapter", + "source_files": [ + "buddy" + ] + }, + { + "name": "C30", + "category": "chapter", + "source_files": [ + "screens", + "outputStyles", + "commands" + ] + }, + { + "name": "C31", + "category": "chapter", + "source_files": [ + "memdir", + "services", + "assistant" + ] + }, + { + "name": "C32", + "category": "chapter", + "source_files": [ + "commands.ts", + "commands" + ] + }, + { + "name": "C33", + "category": "chapter", + "source_files": [ + "state", + "bridge" + ] + }, + { + "name": "C34", + "category": "chapter", + "source_files": [] + }, + { + "name": "CROSSCUT", + "category": "chapter", + "source_files": [ + "context", + "context.ts", + "cost-tracker.ts", + "costHook.ts", + "history.ts", + "ink.ts", + "projectOnboardingState.ts", + "setup.ts", + "types", + "utils", + "moreright" + ] + } + ] +} diff --git a/docs/appendix/F.md b/docs/appendix/F.md new file mode 100644 index 0000000..daece55 --- /dev/null +++ b/docs/appendix/F.md @@ -0,0 +1,51 @@ +# 附录 F · 模块 × 章节 双向矩阵 + +> source_commit: `290fdc9481a70612bc5823aa4ed225c52c52aad3` +> generated_at: `2026-05-21T17:10:29.258Z` +> 章节 35 项;CLI 一级条目 53 个;孤儿 0 个(allowlist=3) + +由 `scripts/gen-module-matrix.ts` 生成。Spec §7.6:`--check-orphans` 在 orphans 数 > 0 时 fail。allowlist 见 `scripts/orphan-allowlist.txt`。 + +## F.1 章节 → 源码一级目录(正向) + +| chapter | claimed top-level entries | +|---|---| +| C01 | `entrypoints`, `bridge`, `remote`, `coordinator`, `buddy`, `upstreamproxy`, `server`, `migrations`, `native-ts`, `screens`, `outputStyles`, `memdir`, `assistant`, `schemas` | +| C02 | `bootstrap`, `main.tsx`, `replLauncher.tsx`, `dialogLaunchers.tsx`, `interactiveHelpers.tsx` | +| C03 | `services` | +| C04 | `migrations` | +| C05 | `QueryEngine.ts`, `query.ts`, `query` | +| C06 | `constants`, `outputStyles` | +| C07 | `services` | +| C08 | `services` | +| C09 | `commands`, `services` | +| C10 | `Tool.ts`, `tools.ts`, `tools` | +| C11 | `tools` | +| C12 | `tools`, `services` | +| C13 | `tools` | +| C14 | `tools`, `services`, `commands` | +| C15 | `tools`, `services` | +| C16 | `Task.ts`, `tasks.ts`, `tasks`, `tools` | +| C17 | `coordinator`, `tools`, `hooks` | +| C18 | `services`, `tools` | +| C19 | `Tool.ts`, `hooks`, `bridge`, `remote` | +| C20 | `schemas`, `hooks`, `query` | +| C21 | `skills`, `services`, `plugins`, `outputStyles` | +| C22 | `utils`, `constants` | +| C23 | `services`, `cli` | +| C24 | `bridge`, `remote`, `commands` | +| C25 | `server`, `upstreamproxy`, `hooks` | +| C26 | `ink`, `native-ts` | +| C27 | `components` | +| C28 | `keybindings`, `vim`, `voice`, `services`, `hooks`, `commands` | +| C29 | `buddy` | +| C30 | `screens`, `outputStyles`, `commands` | +| C31 | `memdir`, `services`, `assistant` | +| C32 | `commands.ts`, `commands` | +| C33 | `state`, `bridge` | +| C34 | — (横切) | +| CROSSCUT | `context`, `context.ts`, `cost-tracker.ts`, `costHook.ts`, `history.ts`, `ink.ts`, `projectOnboardingState.ts`, `setup.ts`, `types`, `utils`, `moreright` | + +## F.2 反向矩阵 + 孤儿 + +✅ 所有 CLI 一级条目均已被章节认领。 diff --git a/package.json b/package.json new file mode 100644 index 0000000..b57863f --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "claude-code-source-study", + "private": true, + "version": "0.0.0", + "description": "V2 修订基础设施:附录 A–F 生成器 + 2 项 CI lint。spec: docs/V2-REVISION-SPEC.md", + "type": "module", + "scripts": { + "gen:appendix": "pnpm gen:A && pnpm gen:B && pnpm gen:C && pnpm gen:D && pnpm gen:E && pnpm gen:F", + "gen:A": "tsx scripts/gen-tool-table.ts", + "gen:B": "tsx scripts/gen-commands-table.ts", + "gen:C": "tsx scripts/gen-hooks-table.ts", + "gen:D": "tsx scripts/gen-agents-table.ts", + "gen:E": "tsx scripts/gen-tasktypes-table.ts", + "gen:F": "tsx scripts/gen-module-matrix.ts", + "check:source-commits": "tsx scripts/check-source-commits.ts", + "check:fuzzy-quantifiers": "tsx scripts/lint-no-fuzzy-quantifiers.ts", + "check:orphans": "tsx scripts/gen-module-matrix.ts --check-orphans", + "check:docs": "pnpm check:source-commits && pnpm check:fuzzy-quantifiers && pnpm check:orphans" + }, + "devDependencies": { + "tsx": "^4.19.0", + "typescript": "^5.5.0", + "@types/node": "^20.11.0" + } +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..7664302 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,48 @@ +# `scripts/` — V2 修订基础设施 + +本目录是 V2-REVISION-SPEC.md §7 规定的基础设施代码。所有脚本由 [`tsx`](https://www.npmjs.com/package/tsx) 直接执行,无需先编译。 + +## 8 个脚本 + +| 脚本 | 角色 | 输出 | +|---|---|---| +| `gen-tool-table.ts` | 附录 A | `docs/appendix/A.{md,manifest.json}` | +| `gen-commands-table.ts` | 附录 B | `docs/appendix/B.{md,manifest.json}` | +| `gen-hooks-table.ts` | 附录 C | `docs/appendix/C.{md,manifest.json}` | +| `gen-agents-table.ts` | 附录 D | `docs/appendix/D.{md,manifest.json}` | +| `gen-tasktypes-table.ts` | 附录 E | `docs/appendix/E.{md,manifest.json}` | +| `gen-module-matrix.ts` | 附录 F + 孤儿反向校验 | `docs/appendix/F.{md,manifest.json}` | +| `check-source-commits.ts` | CI lint §0.3:跨章节 / 跨 manifest commit 一致性 | exit 0/1 | +| `lint-no-fuzzy-quantifiers.ts` | CI lint §0.4:事实段落禁词 | exit 0/1 | + +## 配置 + +每个生成器需要指向 CLI 源码根目录。三种方式(按优先级): + +1. `--source ` 命令行参数 +2. `CLI_SOURCE` 环境变量 +3. 默认值:`~/work/code/awesome-project/claude-code-cli`(spec §0.1) + +可选 `--source-commit ` 强制指定 manifest 写入的 commit;不指定时由 `git -C rev-parse HEAD` 取得。 + +## 本地用法 + +```sh +# 一次性生成全部附录 +CLI_SOURCE=~/work/code/awesome-project/claude-code-cli pnpm gen:appendix + +# 跑齐 3 项 CI 校验(本地与 CI 行为一致) +CLI_SOURCE=~/work/code/awesome-project/claude-code-cli pnpm check:docs +``` + +## CI 契约(§7.4) + +- `check:source-commits` / `check:fuzzy-quantifiers` 仅读取仓内文件,PR 必跑。 +- `gen:appendix` + `check:orphans` 需要 CLI 源码;通过 GitHub Secret `CLI_SOURCE_PATH` 注入。fork PR 自动跳过并打 warning,由维护者推 main 时跑齐全套。 +- `git diff --exit-code docs/appendix` 检测 manifest 漂移:作者必须在 PR 内提交最新 manifest,否则 fail。 + +## 设计取舍 + +- **不解析 TS AST**:附录 A/B/E 用正则在 `tools.ts` / `commands.ts` / `Task.ts` 上抽取注册图。spec 锁定的 commit 不变,正则即足够;当源码结构变更时 manifest 数字也会改变,CI diff 立即可见。 +- **附录 F 章节→模块映射手写**:因为这是「写作合同」(spec §6.2 已敲定),不应试图从源码反推;脚本只做反向 orphan diff。 +- **lint §0.4 跳过附录与 spec**:附录由本目录生成、spec 是元文档,二者本身不参与"事实段落"语义。 diff --git a/scripts/check-source-commits.ts b/scripts/check-source-commits.ts new file mode 100644 index 0000000..32b47c5 --- /dev/null +++ b/scripts/check-source-commits.ts @@ -0,0 +1,111 @@ +/** + * CI lint #1 — `check-source-commits` (V2-REVISION-SPEC §0.3, §7.4). + * + * Walks every chapter file under `docs/` and every appendix manifest under + * `docs/appendix/`. Pulls the declared `source_commit` from each: + * - chapter `*.md` → YAML frontmatter `source_commit:` field + * (or §0.1 「源码锚点」block: `源码版本:` / `source_commit: `) + * - manifest `.json` → `source_commit` JSON field + * + * Fail when the declared commits diverge across files. Spec wording: + * 「正文与 manifest 引用必须指向同一 commit」. + * + * Exit code 1 on mismatch; informative table on stderr. + * + * Allowed exception: chapter files that have not yet been authored may omit + * the field; we simply skip them. Once a chapter declares any commit it + * must match the rest. + */ + +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' +import * as path from 'node:path' + +const repoRoot = process.cwd() +const docsDir = path.join(repoRoot, 'docs') +const appendixDir = path.join(docsDir, 'appendix') + +interface Decl { + file: string + commit: string +} + +const decls: Decl[] = [] + +function walkDocs(dir: string): void { + if (!existsSync(dir)) return + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const abs = path.join(dir, entry.name) + if (entry.isDirectory()) { + if (entry.name === 'appendix') continue // handled separately + walkDocs(abs) + continue + } + if (!entry.isFile()) continue + if (!entry.name.endsWith('.md')) continue + const text = readFileSync(abs, 'utf8') + // YAML frontmatter + const fm = text.match(/^---\n([\s\S]*?)\n---/) + if (fm) { + const sm = fm[1].match(/^source_commit:\s*([0-9a-f]{7,40})/m) + if (sm) { + decls.push({ file: path.relative(repoRoot, abs), commit: sm[1] }) + continue + } + } + // §0.1 「源码版本:」 / 「source_commit: 」 inline + const inline = + text.match(/源码版本[::]\s*`?([0-9a-f]{7,40})/) || + text.match(/source_commit[::]\s*`?([0-9a-f]{7,40})/) + if (inline) { + decls.push({ file: path.relative(repoRoot, abs), commit: inline[1] }) + } + } +} + +walkDocs(docsDir) + +if (existsSync(appendixDir)) { + for (const entry of readdirSync(appendixDir)) { + if (!entry.endsWith('.manifest.json')) continue + const abs = path.join(appendixDir, entry) + const json = JSON.parse(readFileSync(abs, 'utf8')) as { source_commit?: string } + if (json.source_commit) { + decls.push({ file: path.relative(repoRoot, abs), commit: json.source_commit }) + } + } +} + +if (decls.length === 0) { + process.stderr.write('check-source-commits: no source_commit declarations found (skipping)\n') + process.exit(0) +} + +// Group by canonical commit (take min length as canonical for short-vs-full +// SHA matches: full prefix-equality wins). +const groups = new Map() +for (const d of decls) { + groups.set(d.commit, [...(groups.get(d.commit) ?? []), d.file]) +} + +// Two distinct full commits → fail. But short-prefix of long is allowed. +const longest = [...groups.keys()].sort((a, b) => b.length - a.length)[0] +let bad: string[] = [] +for (const [commit, files] of groups) { + if (!longest.startsWith(commit) && !commit.startsWith(longest)) { + bad.push(`commit ${commit} (${files.length} files): ${files.slice(0, 4).join(', ')}${files.length > 4 ? `, +${files.length - 4}` : ''}`) + } +} + +if (bad.length > 0) { + process.stderr.write( + `check-source-commits FAIL — multiple incompatible source_commit values:\n` + + ` canonical: ${longest}\n` + + bad.map(b => ` divergent: ${b}\n`).join('') + + `Spec §0.3 forbids per-file commit drift; open one PR to bump every chapter + manifest together.\n`, + ) + process.exit(1) +} + +process.stderr.write( + `check-source-commits OK — ${decls.length} declarations all align with ${longest}\n`, +) diff --git a/scripts/gen-agents-table.ts b/scripts/gen-agents-table.ts new file mode 100644 index 0000000..2c38f10 --- /dev/null +++ b/scripts/gen-agents-table.ts @@ -0,0 +1,158 @@ +/** + * Appendix D · Built-in Agent speed-sheet (V2-REVISION-SPEC §7.1, §7.5). + * + * Two-tier model per §7.5: + * 1. Positive table — every Agent **defined in source**: scan + * `tools/AgentTool/built-in/*Agent*.ts` and `coordinator/workerAgent.ts`, + * pull `agentType` / `model` / `tools` from the literal export. + * 2. Side-table notes — for each agent, list the variables that affect + * runtime availability (feature flags, entrypoint gate, coordinator). + * `getBuiltInAgents()` in `tools/AgentTool/builtInAgents.ts` is the + * authority for these conditions. + */ + +import { existsSync, readFileSync } from 'node:fs' +import * as path from 'node:path' +import { + diffSummary, + ensureSourceDir, + listFiles, + loadManifest, + parseArgs, + resolveSourceCommit, + writeManifestAndTable, + type Manifest, + type ManifestItem, +} from './lib/util.js' + +const args = parseArgs(process.argv) +ensureSourceDir(args) +const sourceCommit = resolveSourceCommit(args) + +const builtInDir = path.join(args.source, 'tools/AgentTool/built-in') +const registryFile = path.join(args.source, 'tools/AgentTool/builtInAgents.ts') +const registryText = existsSync(registryFile) ? readFileSync(registryFile, 'utf8') : '' + +interface AgentRecord { + symbol: string // exported const name, e.g. GENERAL_PURPOSE_AGENT + rel: string // relative path + agentType: string | null + model: string | null + tools: string | null + whenToUseLine: number | null +} + +function pullField(text: string, field: string): string | null { + // Match `field: 'value'` or `field: "value"` at start of any line. + const re = new RegExp(`^\\s*${field}:\\s*(?:'([^']*)'|"([^"]*)"|([^,\\n]+))`, 'm') + const m = text.match(re) + if (!m) return null + return (m[1] ?? m[2] ?? (m[3] ?? '').trim().replace(/[,]+$/, '')) || null +} + +function findExportedSymbol(text: string): string | null { + const m = text.match(/export const ([A-Z_][A-Z0-9_]*)\s*:/) + return m ? m[1] : null +} + +const agentFiles = listFiles(builtInDir, n => /\.ts$/.test(n)) +const agents: AgentRecord[] = [] + +for (const f of agentFiles) { + const abs = path.join(builtInDir, f) + const text = readFileSync(abs, 'utf8') + const sym = findExportedSymbol(text) + if (!sym) continue + const agentType = pullField(text, 'agentType') + const model = pullField(text, 'model') + // tools is an array literal — capture verbatim, sliced. + const toolsM = text.match(/^\s*tools:\s*\[([^\]]*)\]/m) + const tools = toolsM ? toolsM[1].trim() : null + // Locate the line `whenToUse:` for source citation. + let whenToUseLine: number | null = null + const lines = text.split('\n') + for (let i = 0; i < lines.length; i++) { + if (/^\s*whenToUse:/.test(lines[i])) { + whenToUseLine = i + 1 + break + } + } + agents.push({ + symbol: sym, + rel: `tools/AgentTool/built-in/${f}`, + agentType, + model, + tools, + whenToUseLine, + }) +} + +// Cross-reference with builtInAgents.ts to determine gating. +const gateRules: { symbol: string; gates: string[] }[] = [] +for (const a of agents) { + const gates: string[] = [] + // Default-on agents listed unconditionally in `agents` array literal. + // Conservative detection: search registry text for the symbol context. + const idx = registryText.indexOf(a.symbol) + if (idx === -1) { + // Imported but never pushed → likely coordinator-only or unused. + gates.push('not-default') + } else { + // Walk backward to find nearest `feature(` or `if (` clause within ~400 chars. + const window = registryText.slice(Math.max(0, idx - 400), idx) + const featRe = /feature\('([A-Z_]+)'\)/g + let fm: RegExpExecArray | null + while ((fm = featRe.exec(window)) !== null) gates.push(`feature:${fm[1]}`) + if (/CLAUDE_CODE_ENTRYPOINT/.test(window)) gates.push('entrypoint:non-sdk') + if (/CLAUDE_CODE_COORDINATOR_MODE/.test(window)) gates.push('coordinator-mode') + if (/areExplorePlanAgentsEnabled/.test(window)) gates.push('feature:BUILTIN_EXPLORE_PLAN_AGENTS') + } + gateRules.push({ symbol: a.symbol, gates: Array.from(new Set(gates)) }) +} + +const items: ManifestItem[] = agents.map((a, i) => ({ + name: a.agentType ?? a.symbol, + category: 'built-in', + source_files: [ + a.whenToUseLine ? `${a.rel}:${a.whenToUseLine}` : a.rel, + ], + feature_flags: gateRules[i].gates.length ? gateRules[i].gates : undefined, + notes: [ + a.model ? `model=${a.model}` : null, + a.tools !== null ? `tools=[${a.tools}]` : null, + ] + .filter(Boolean) + .join('; ') || undefined, +})) + +const manifest: Manifest = { + generated_at: new Date().toISOString(), + source_commit: sourceCommit, + appendix: 'D', + items, +} + +const md = [ + '# 附录 D · 内置 Agent 速查表', + '', + `> source_commit: \`${sourceCommit}\``, + `> generated_at: \`${manifest.generated_at}\``, + `> 共 ${items.length} 个源码定义的 built-in agent`, + '', + '本表只列「源码定义」的 Agent;运行时是否启用受 feature flag / entrypoint / coordinator 影响——见 `gates` 列。正文 §C15 必须声明这一两段式(spec §7.5)。', + '', + '| agentType (or symbol) | source_file | gates (feature/entry/coord) | notes |', + '|---|---|---|---|', + ...items.map(i => + `| \`${i.name}\` | ${i.source_files[0]} | ${(i.feature_flags ?? []).join(', ') || '—'} | ${i.notes ?? '—'} |`, + ), + '', +].join('\n') + +const prev = loadManifest(path.join(args.out, 'D.manifest.json')) +const { manifestPath, tablePath } = writeManifestAndTable(args, 'D', manifest, md) + +if (args.diffSummary) { + process.stdout.write(`appendix-D: ${diffSummary(prev, manifest)}\n`) +} +process.stderr.write(`wrote ${manifestPath} (${items.length} items)\nwrote ${tablePath}\n`) diff --git a/scripts/gen-commands-table.ts b/scripts/gen-commands-table.ts new file mode 100644 index 0000000..9eb4974 --- /dev/null +++ b/scripts/gen-commands-table.ts @@ -0,0 +1,136 @@ +/** + * Appendix B · Commands speed-sheet generator (V2-REVISION-SPEC §7.1). + * + * Walks `commands/` under the CLI source root and partitions entries into: + * - `top-level-dir` : a subdirectory holding a command implementation + * - `top-level-file` : a single `.ts` / `.tsx` / `.js` / `.jsx` / `.mjs` / + * `.cjs` file in `commands/` + * - `runtime-cmd` : commands explicitly enumerated by `commands.ts` + * + * Several runtime commands ship as `.js` (e.g. `commands/ant-trace/index.js`, + * `commands/autofix-pr/`, `backfill-sessions/`, `break-cache/`, `bughunter/`), + * so the file filter accepts the full JavaScript/TypeScript extension family + * to keep `source_files` populated per spec §7.3 `path:line-line` contract. + * + * Spec §C32 forbids hard-coding "X runtime commands" in the doc; the appendix + * stays the single source of truth. + */ + +import * as path from 'node:path' +import { + countLines, + diffSummary, + ensureSourceDir, + listFiles, + listSubdirs, + loadManifest, + parseArgs, + readText, + resolveSourceCommit, + writeManifestAndTable, + type Manifest, + type ManifestItem, +} from './lib/util.js' + +const args = parseArgs(process.argv) +ensureSourceDir(args) +const sourceCommit = resolveSourceCommit(args) + +const commandsRoot = path.join(args.source, 'commands') +const commandsTs = path.join(args.source, 'commands.ts') +const commandsTsText = readText(commandsTs) + +const dirs = listSubdirs(commandsRoot) +// Source files: accept the full JS/TS extension family — runtime commands like +// `commands/autofix-pr/index.js` ship as `.js`, so a `.ts|.tsx`-only filter +// produces empty `source_files` (cf. OC-R blocker on B.manifest.json:61-106). +const SOURCE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/ +const topLevelFiles = listFiles(commandsRoot, n => SOURCE_EXT_RE.test(n) && !/\.test\./.test(n)) + +// Collect symbol-style command identifiers referenced by commands.ts. The +// CLI registers commands by importing files like `./commands/foo/foo.js` or +// directly by symbol; we extract both shapes to mark `runtime-cmd`. +const importPathRe = /from '\.\/commands\/([A-Za-z0-9._-]+)(?:\/([A-Za-z0-9._-]+))?/g +const referenced: Set = new Set() +let m: RegExpExecArray | null +while ((m = importPathRe.exec(commandsTsText)) !== null) { + referenced.add(m[1]) +} + +const items: ManifestItem[] = [] + +function fileWithLines(rel: string, abs: string): string { + const lines = countLines(abs) + return `${rel}:1-${Math.max(lines, 1)}` +} + +for (const dir of dirs) { + const isReferenced = referenced.has(dir) + const subFiles = listFiles(path.join(commandsRoot, dir), n => SOURCE_EXT_RE.test(n) && !/\.test\./.test(n)) + items.push({ + name: dir, + category: isReferenced ? 'runtime-cmd' : 'top-level-dir', + source_files: subFiles.map(f => + fileWithLines(`commands/${dir}/${f}`, path.join(commandsRoot, dir, f)), + ), + notes: isReferenced ? 'imported by commands.ts' : undefined, + }) +} + +for (const file of topLevelFiles) { + const base = file.replace(SOURCE_EXT_RE, '') + // commands.ts itself is the registry, not a command; skip if it wandered in. + if (file === 'commands.ts' || file === 'commands.tsx') continue + items.push({ + name: base, + category: 'top-level-file', + source_files: [fileWithLines(`commands/${file}`, path.join(commandsRoot, file))], + }) +} + +// Stable sort: dirs first by name, then files by name. +items.sort((a, b) => { + if (a.category !== b.category) { + if (a.category === 'top-level-dir' || a.category === 'runtime-cmd') return -1 + return 1 + } + return a.name.localeCompare(b.name) +}) + +const manifest: Manifest = { + generated_at: new Date().toISOString(), + source_commit: sourceCommit, + appendix: 'B', + items, +} + +const counts = { + dir: items.filter(i => i.category === 'top-level-dir').length, + runtime: items.filter(i => i.category === 'runtime-cmd').length, + file: items.filter(i => i.category === 'top-level-file').length, +} + +const md = [ + '# 附录 B · Commands 速查表', + '', + `> source_commit: \`${sourceCommit}\``, + `> generated_at: \`${manifest.generated_at}\``, + `> 共 ${items.length} 项(top-level-dir=${counts.dir} / runtime-cmd=${counts.runtime} / top-level-file=${counts.file})`, + '', + '由 `scripts/gen-commands-table.ts` 扫描 `commands/` 一级目录、`commands/*.ts` 一级文件,并交叉 `commands.ts` 注册图。正文 §C32 引用本表,不裸写命令数。', + '', + '| name | category | source_files |', + '|---|---|---|', + ...items.map(i => + `| \`${i.name}\` | ${i.category} | ${i.source_files.slice(0, 3).join(', ')}${i.source_files.length > 3 ? ` …(+${i.source_files.length - 3})` : ''} |`, + ), + '', +].join('\n') + +const prev = loadManifest(path.join(args.out, 'B.manifest.json')) +const { manifestPath, tablePath } = writeManifestAndTable(args, 'B', manifest, md) + +if (args.diffSummary) { + process.stdout.write(`appendix-B: ${diffSummary(prev, manifest)}\n`) +} +process.stderr.write(`wrote ${manifestPath} (${items.length} items)\nwrote ${tablePath}\n`) diff --git a/scripts/gen-hooks-table.ts b/scripts/gen-hooks-table.ts new file mode 100644 index 0000000..fd49345 --- /dev/null +++ b/scripts/gen-hooks-table.ts @@ -0,0 +1,123 @@ +/** + * Appendix C · Hooks event table generator (V2-REVISION-SPEC §7.1). + * + * Two distinct surfaces: + * 1. The wire-level event names exported as `HOOK_EVENTS` (currently 27) + * from `entrypoints/sdk/coreTypes.ts`. + * 2. The hook *command* shape — declared by `schemas/hooks.ts` and the + * `hooks/` runtime — currently 4 kinds: `command` / `prompt` / `http` / + * `agent`. + * + * Spec §C20 requires the doc to lock both surfaces against this manifest. + */ + +import * as path from 'node:path' +import { + diffSummary, + ensureSourceDir, + loadManifest, + parseArgs, + readText, + resolveSourceCommit, + writeManifestAndTable, + type Manifest, + type ManifestItem, +} from './lib/util.js' + +const args = parseArgs(process.argv) +ensureSourceDir(args) +const sourceCommit = resolveSourceCommit(args) + +const eventsFile = path.join(args.source, 'entrypoints/sdk/coreTypes.ts') +const eventsText = readText(eventsFile) + +// Locate `export const HOOK_EVENTS = [ … ] as const` and parse the literal +// list. We do not eval; we slice between `[` and `]` and pull single-quoted +// identifiers. +function extractEvents(text: string): { name: string; line: number }[] { + const startMatch = text.match(/export const HOOK_EVENTS = \[/) + if (!startMatch) { + throw new Error('HOOK_EVENTS literal not found in coreTypes.ts') + } + const startIdx = startMatch.index! + startMatch[0].length + const endIdx = text.indexOf(']', startIdx) + if (endIdx === -1) throw new Error('unterminated HOOK_EVENTS literal') + const slice = text.slice(startIdx, endIdx) + const lines = text.slice(0, startIdx).split('\n').length + const out: { name: string; line: number }[] = [] + const sliceLines = slice.split('\n') + for (let i = 0; i < sliceLines.length; i++) { + const m = sliceLines[i].match(/'([A-Za-z0-9_]+)'/) + if (m) out.push({ name: m[1], line: lines + i }) + } + return out +} + +const events = extractEvents(eventsText) + +// Hook command kinds: enumerate by inspecting `schemas/hooks.ts` for kind +// discriminator string literals — the schema uses +// `z.discriminatedUnion('type', […])` per command. Pull every `type: z.literal('X')`. +const schemasFile = path.join(args.source, 'schemas/hooks.ts') +const schemasText = readText(schemasFile) +const kindRe = /type:\s*z\.literal\(['"]([a-z_]+)['"]\)/g +const kindSet = new Set() +let km: RegExpExecArray | null +while ((km = kindRe.exec(schemasText)) !== null) kindSet.add(km[1]) +const kinds = Array.from(kindSet).sort() + +const items: ManifestItem[] = [] +for (const ev of events) { + items.push({ + name: ev.name, + category: 'event', + source_files: [`entrypoints/sdk/coreTypes.ts:${ev.line}`], + wire_type: 'hook_event', + }) +} +for (const k of kinds) { + items.push({ + name: k, + category: 'command-kind', + source_files: [`schemas/hooks.ts`], + wire_type: 'hook_command', + }) +} + +const manifest: Manifest = { + generated_at: new Date().toISOString(), + source_commit: sourceCommit, + appendix: 'C', + items, +} + +const md = [ + '# 附录 C · Hooks 事件表', + '', + `> source_commit: \`${sourceCommit}\``, + `> generated_at: \`${manifest.generated_at}\``, + `> 共 ${events.length} 个 HOOK_EVENTS + ${kinds.length} 种 hook command`, + '', + '由 `scripts/gen-hooks-table.ts` 扫描 `entrypoints/sdk/coreTypes.ts` (HOOK_EVENTS 字面量) 与 `schemas/hooks.ts`(命令 kind 判别)。正文 §C20 不裸写"27",引用本表。', + '', + '## C.1 HOOK_EVENTS', + '', + '| # | name | source |', + '|---|---|---|', + ...events.map((e, i) => `| ${i + 1} | \`${e.name}\` | entrypoints/sdk/coreTypes.ts:${e.line} |`), + '', + '## C.2 Hook command kinds', + '', + '| name | source |', + '|---|---|', + ...kinds.map(k => `| \`${k}\` | schemas/hooks.ts |`), + '', +].join('\n') + +const prev = loadManifest(path.join(args.out, 'C.manifest.json')) +const { manifestPath, tablePath } = writeManifestAndTable(args, 'C', manifest, md) + +if (args.diffSummary) { + process.stdout.write(`appendix-C: ${diffSummary(prev, manifest)}\n`) +} +process.stderr.write(`wrote ${manifestPath} (${items.length} items)\nwrote ${tablePath}\n`) diff --git a/scripts/gen-module-matrix.ts b/scripts/gen-module-matrix.ts new file mode 100644 index 0000000..e2b2a2e --- /dev/null +++ b/scripts/gen-module-matrix.ts @@ -0,0 +1,176 @@ +/** + * Appendix F · 模块 × 章节 双向矩阵 + 孤儿目录反向校验 + * (V2-REVISION-SPEC §7.1, §7.6). + * + * Two outputs in one manifest: + * 1. Forward map: each v2 chapter → list of CLI top-level directories / + * files it owns. Hard-coded here from V2-REVISION-SPEC.md §6.2 because + * the chapter→module mapping is an authoring contract, not a fact you + * can derive from the source tree. + * 2. Reverse map / orphan check: every top-level directory in the CLI + * source must be claimed by ≥ 1 chapter. Difference set is the orphan + * list; an allowlist file `scripts/orphan-allowlist.txt` (one entry per + * line, `#` comments) suppresses known exceptions. + * + * Spec §7.6: orphan list non-empty (after allowlist) → `--check-orphans` exit 1. + */ + +import { existsSync, readFileSync } from 'node:fs' +import * as path from 'node:path' +import { + diffSummary, + ensureSourceDir, + listFiles, + listSubdirs, + loadManifest, + parseArgs, + resolveSourceCommit, + writeManifestAndTable, + type Manifest, + type ManifestItem, +} from './lib/util.js' + +const args = parseArgs(process.argv) +ensureSourceDir(args) +const sourceCommit = resolveSourceCommit(args) + +// Encoded from V2-REVISION-SPEC.md §5 + §6.2 reverse matrix. +// Each entry: chapter → list of top-level directory names OR top-level file +// names (with extension) that the chapter is the canonical owner of. +const CHAPTER_TO_MODULES: Record = { + C01: [ + 'entrypoints', 'bridge', 'remote', 'coordinator', 'buddy', + 'upstreamproxy', 'server', 'migrations', 'native-ts', 'screens', + 'outputStyles', 'memdir', 'assistant', 'schemas', + ], + C02: ['bootstrap', 'main.tsx', 'replLauncher.tsx', 'dialogLaunchers.tsx', 'interactiveHelpers.tsx'], + C03: ['services'], + C04: ['migrations'], + C05: ['QueryEngine.ts', 'query.ts', 'query'], + C06: ['constants', 'outputStyles'], + C07: ['services'], + C08: ['services'], + C09: ['commands', 'services'], + C10: ['Tool.ts', 'tools.ts', 'tools'], + C11: ['tools'], + C12: ['tools', 'services'], + C13: ['tools'], + C14: ['tools', 'services', 'commands'], + C15: ['tools', 'services'], + C16: ['Task.ts', 'tasks.ts', 'tasks', 'tools'], + C17: ['coordinator', 'tools', 'hooks'], + C18: ['services', 'tools'], + C19: ['Tool.ts', 'hooks', 'bridge', 'remote'], + C20: ['schemas', 'hooks', 'query'], + C21: ['skills', 'services', 'plugins', 'outputStyles'], + C22: ['utils', 'constants'], + C23: ['services', 'cli'], + C24: ['bridge', 'remote', 'commands'], + C25: ['server', 'upstreamproxy', 'hooks'], + C26: ['ink', 'native-ts'], + C27: ['components'], + C28: ['keybindings', 'vim', 'voice', 'services', 'hooks', 'commands'], + C29: ['buddy'], + C30: ['screens', 'outputStyles', 'commands'], + C31: ['memdir', 'services', 'assistant'], + C32: ['commands.ts', 'commands'], + C33: ['state', 'bridge'], + C34: [], + // Cross-cutting / runtime-glue files that belong to no single chapter + // but must still be claimed to keep the orphan check honest: + CROSSCUT: [ + 'context', 'context.ts', 'cost-tracker.ts', 'costHook.ts', + 'history.ts', 'ink.ts', 'projectOnboardingState.ts', 'setup.ts', + 'types', 'utils', 'moreright', + ], +} + +// Build forward + reverse views. +const claimed = new Set() +for (const mods of Object.values(CHAPTER_TO_MODULES)) { + for (const m of mods) claimed.add(m) +} + +// Discover the actual top-level entries of the CLI source root. +const cliRoot = args.source +const topDirs = listSubdirs(cliRoot) +const topFiles = listFiles(cliRoot, n => /\.(ts|tsx)$/.test(n) && !/\.test\./.test(n)) +const topEntries = [...topDirs, ...topFiles] + +// Allowlist for orphan check. +const allowlistPath = path.join('scripts', 'orphan-allowlist.txt') +const allowlist: string[] = existsSync(allowlistPath) + ? readFileSync(allowlistPath, 'utf8') + .split('\n') + .map(l => l.replace(/#.*$/, '').trim()) + .filter(Boolean) + : [] + +const orphans = topEntries.filter(e => !claimed.has(e) && !allowlist.includes(e)) + +const items: ManifestItem[] = [] +for (const [chapter, mods] of Object.entries(CHAPTER_TO_MODULES)) { + items.push({ + name: chapter, + category: 'chapter', + source_files: mods, + }) +} +for (const orphan of orphans) { + items.push({ + name: orphan, + category: 'orphan', + source_files: [orphan], + notes: 'no v2 chapter claims this directory/file', + }) +} + +const manifest: Manifest = { + generated_at: new Date().toISOString(), + source_commit: sourceCommit, + appendix: 'F', + items, +} + +const md = [ + '# 附录 F · 模块 × 章节 双向矩阵', + '', + `> source_commit: \`${sourceCommit}\``, + `> generated_at: \`${manifest.generated_at}\``, + `> 章节 ${Object.keys(CHAPTER_TO_MODULES).length} 项;CLI 一级条目 ${topEntries.length} 个;孤儿 ${orphans.length} 个(allowlist=${allowlist.length})`, + '', + '由 `scripts/gen-module-matrix.ts` 生成。Spec §7.6:`--check-orphans` 在 orphans 数 > 0 时 fail。allowlist 见 `scripts/orphan-allowlist.txt`。', + '', + '## F.1 章节 → 源码一级目录(正向)', + '', + '| chapter | claimed top-level entries |', + '|---|---|', + ...Object.entries(CHAPTER_TO_MODULES).map(([c, mods]) => `| ${c} | ${mods.length ? mods.map(m => `\`${m}\``).join(', ') : '— (横切)'} |`), + '', + '## F.2 反向矩阵 + 孤儿', + '', + orphans.length === 0 + ? '✅ 所有 CLI 一级条目均已被章节认领。' + : ['❌ 以下条目尚未被任何章节认领:', '', ...orphans.map(o => `- \`${o}\``)].join('\n'), + '', +].join('\n') + +const prev = loadManifest(path.join(args.out, 'F.manifest.json')) +const { manifestPath, tablePath } = writeManifestAndTable(args, 'F', manifest, md) + +if (args.diffSummary) { + process.stdout.write(`appendix-F: ${diffSummary(prev, manifest)}\n`) +} +process.stderr.write(`wrote ${manifestPath} (${items.length} items, orphans=${orphans.length})\nwrote ${tablePath}\n`) + +if (args.checkOrphans) { + if (orphans.length > 0) { + process.stderr.write( + `gen-module-matrix --check-orphans FAIL: ${orphans.length} orphan top-level entries:\n` + + orphans.map(o => ` - ${o}`).join('\n') + + '\nAdd a chapter claim or list it in scripts/orphan-allowlist.txt with a comment.\n', + ) + process.exit(1) + } + process.stderr.write('gen-module-matrix --check-orphans OK (0 orphans)\n') +} diff --git a/scripts/gen-tasktypes-table.ts b/scripts/gen-tasktypes-table.ts new file mode 100644 index 0000000..571bec6 --- /dev/null +++ b/scripts/gen-tasktypes-table.ts @@ -0,0 +1,176 @@ +/** + * Appendix E · TaskType 谱系生成器 (V2-REVISION-SPEC §7.1). + * + * Authority for the wire-level TaskType union: `Task.ts:6-13` (the + * `export type TaskType = | 'local_bash' | …` literal). Default vs + * feature-gated registration: `tasks.ts` `getAllTasks()` body. + * + * Spec §C16 calls for a "7 wire / 4 default + 2 feature-gated + 1 in-process + * special" classification — this script extracts that classification from + * source rather than asserting it. + */ + +import { existsSync } from 'node:fs' +import * as path from 'node:path' +import { + countLines, + diffSummary, + ensureSourceDir, + loadManifest, + parseArgs, + readText, + resolveSourceCommit, + writeManifestAndTable, + type Manifest, + type ManifestItem, +} from './lib/util.js' + +const args = parseArgs(process.argv) +ensureSourceDir(args) +const sourceCommit = resolveSourceCommit(args) + +const taskTsAbs = path.join(args.source, 'Task.ts') +const tasksTsAbs = path.join(args.source, 'tasks.ts') +const taskTs = readText(taskTsAbs) +const tasksTs = readText(tasksTsAbs) + +// Pull wire types from the `TaskType` union literal. The union has no +// trailing `;` (TS allows the implicit terminator before a blank line), +// so we slice from the start marker until the next blank line. +const unionStart = taskTs.search(/export type TaskType =/) +if (unionStart === -1) throw new Error('TaskType declaration not found in Task.ts') +const unionEnd = taskTs.indexOf('\n\n', unionStart) +if (unionEnd === -1) throw new Error('TaskType union has no terminating blank line') +const unionBody = taskTs.slice(unionStart, unionEnd) +// 1-indexed start/end line of the union literal in Task.ts (used as +// `Task.ts:start-end` anchor for every wire type entry below). +const unionStartLine = taskTs.slice(0, unionStart).split('\n').length +const unionEndLine = taskTs.slice(0, unionEnd).split('\n').length +const taskTsAnchor = `Task.ts:${unionStartLine}-${unionEndLine}` +const wireTypes: string[] = [] +const lit = unionBody +const litRe = /'([a-z_]+)'/g +let lm: RegExpExecArray | null +while ((lm = litRe.exec(lit)) !== null) wireTypes.push(lm[1]) + +// Find which task implementations are unconditionally pushed in +// `getAllTasks()` vs guarded by `feature(...)`. +function isFeatureGated(typeName: string, body: string): { gated: boolean; flag?: string } { + // Search for `feature('FLAG')` lines that conditionally assign the task. + // tasks.ts pattern: `const LocalWorkflowTask: Task | null = feature('WORKFLOW_SCRIPTS')` + const camel = typeNameToClass(typeName) + if (!camel) return { gated: false } + const re = new RegExp(`const ${camel}.*?=\\s*feature\\('([A-Z_]+)'\\)`) + const m = body.match(re) + if (m) return { gated: true, flag: m[1] } + return { gated: false } +} + +function typeNameToClass(typeName: string): string | null { + // Map wire type names → class symbols imported / required by tasks.ts. + const map: Record = { + local_bash: 'LocalShellTask', + local_agent: 'LocalAgentTask', + remote_agent: 'RemoteAgentTask', + in_process_teammate: 'InProcessTeammateTask', + local_workflow: 'LocalWorkflowTask', + monitor_mcp: 'MonitorMcpTask', + dream: 'DreamTask', + } + return map[typeName] ?? null +} + +function findInProcessSpecial(typeName: string): boolean { + // in_process_teammate is the documented in-process special: it is *not* + // in `getAllTasks()` (no Task implementation pushed) but appears in the + // wire union and has its own task class file. + return typeName === 'in_process_teammate' +} + +function resolveTaskFile(typeName: string): string { + // Map wire type → on-disk implementation file when the convention holds: + // `tasks//.ts`. If the file is missing (e.g. wire-only + // entries with no separate Task class), anchor to the Task.ts union + // literal so the manifest still meets §7.3's `path:line-line` contract. + const cls = typeNameToClass(typeName) + if (cls) { + const rel = `tasks/${cls}/${cls}.ts` + const abs = path.join(args.source, rel) + if (existsSync(abs)) { + const lines = countLines(abs) + return `${rel}:1-${Math.max(lines, 1)}` + } + } + return taskTsAnchor +} + +const items: ManifestItem[] = wireTypes.map(t => { + const sourceAnchor = resolveTaskFile(t) + if (findInProcessSpecial(t)) { + return { + name: t, + category: 'in-process-special', + source_files: [sourceAnchor], + wire_type: t, + default_registered: false, + notes: 'wire type only; runs in-process via teammate path, not via getAllTasks()', + } + } + const gate = isFeatureGated(t, tasksTs) + if (gate.gated) { + return { + name: t, + category: 'feature-gated', + source_files: [sourceAnchor], + wire_type: t, + default_registered: false, + feature_flags: gate.flag ? [gate.flag] : undefined, + } + } + return { + name: t, + category: 'default', + source_files: [sourceAnchor], + wire_type: t, + default_registered: true, + } +}) + +const counts = { + total: items.length, + default: items.filter(i => i.category === 'default').length, + gated: items.filter(i => i.category === 'feature-gated').length, + inproc: items.filter(i => i.category === 'in-process-special').length, +} + +const manifest: Manifest = { + generated_at: new Date().toISOString(), + source_commit: sourceCommit, + appendix: 'E', + items, +} + +const md = [ + '# 附录 E · TaskType 谱系', + '', + `> source_commit: \`${sourceCommit}\``, + `> generated_at: \`${manifest.generated_at}\``, + `> 共 ${counts.total} 个 wire TaskType(default=${counts.default} / feature-gated=${counts.gated} / in-process-special=${counts.inproc})`, + '', + '由 `scripts/gen-tasktypes-table.ts` 解析 `Task.ts` 的 `TaskType` union 字面量与 `tasks.ts` 的 `getAllTasks()` 注册体。正文 §C16 引用本表,禁止裸写 4/2/1。', + '', + '| name | category | feature_flags | default_registered | source_file | notes |', + '|---|---|---|---|---|---|', + ...items.map(i => + `| \`${i.name}\` | ${i.category} | ${(i.feature_flags ?? []).join(', ') || '—'} | ${String(i.default_registered)} | ${i.source_files[0]} | ${i.notes ?? '—'} |`, + ), + '', +].join('\n') + +const prev = loadManifest(path.join(args.out, 'E.manifest.json')) +const { manifestPath, tablePath } = writeManifestAndTable(args, 'E', manifest, md) + +if (args.diffSummary) { + process.stdout.write(`appendix-E: ${diffSummary(prev, manifest)}\n`) +} +process.stderr.write(`wrote ${manifestPath} (${items.length} items)\nwrote ${tablePath}\n`) diff --git a/scripts/gen-tool-table.ts b/scripts/gen-tool-table.ts new file mode 100644 index 0000000..59d4c5a --- /dev/null +++ b/scripts/gen-tool-table.ts @@ -0,0 +1,429 @@ +/** + * Appendix A · Tool speed-sheet generator (V2-REVISION-SPEC §7.1). + * + * Walks `tools/` directly under the CLI source root, classifies each top-level + * `*Tool/` directory as one of: + * - `family` → the directory contains 2+ leaf `*Tool.ts` files + * (e.g. ScheduleCronTool/{CronCreate,CronDelete,CronList}Tool.ts) + * - `runtime-leaf` → registered unconditionally in `getAllBaseTools()` + * - `feature-gated` → registered behind `feature(...)` / `process.env...` / + * `isXxxEnabled()` / a conditionally-required symbol + * + * Source of truth for runtime registration: `tools.ts` at the CLI source + * root. Both top-level `const FooTool = feature(...) ? require(...) : null` + * declarations *and* inline `getAllBaseTools()` spreads + * (`...(GATE ? [Tool] : [])`) are treated as runtime gates so a directly + * imported tool that is only conditionally added (e.g. `ConfigTool`, + * `LSPTool`) is correctly classified `feature-gated`, not `runtime-leaf`. + * + * Spec §C10 explicitly forbids hard-coded "42 / 43" tool counts; numbers + * in the doc must always be re-derivable from this manifest. + * + * Manifest §7.3 contract: `source_files` entries are `"path:line-line"` + * (1-indexed, inclusive) so chapter facts can anchor on `file:line`. + */ + +import * as path from 'node:path' +import { + countLines, + diffSummary, + ensureSourceDir, + listFiles, + listSubdirs, + loadManifest, + parseArgs, + readText, + resolveSourceCommit, + walkTsFiles, + writeManifestAndTable, + type Manifest, + type ManifestItem, +} from './lib/util.js' + +const args = parseArgs(process.argv) +ensureSourceDir(args) +const sourceCommit = resolveSourceCommit(args) + +const toolsRoot = path.join(args.source, 'tools') +const toolsTs = path.join(args.source, 'tools.ts') +const toolsTsText = readText(toolsTs) + +interface ToolDef { + name: string + category: 'family' | 'runtime-leaf' | 'feature-gated' + source_files: string[] + feature_flags?: string[] + notes?: string +} + +// --------------------------------------------------------------------------- +// Phase 1 · Parse tools.ts to learn (a) which tool symbols map to which +// `tools/` directory, and (b) what gate (if any) each symbol carries. +// We honor two declaration shapes: +// - `const FooTool = feature('X') ? require('./tools/Foo/FooTool.js').FooTool : null` +// - `const FooTool = process.env.X === 'y' ? require('./tools/Foo/...').FooTool : null` +// and import-style symbols (`import { FooTool } from './tools/Foo/FooTool.js'`) +// which have no declaration-level gate. +// --------------------------------------------------------------------------- + +interface SymbolInfo { + /** `tools/` this symbol resolves into (best-effort). */ + dir: string | null + /** Declaration-level gate flags (`feature` / `env:` / `fn:` / null if unconditional). */ + declGates: string[] +} + +const symbolToInfo = new Map() + +function recordSymbol(symbol: string, dir: string | null, gates: string[]): void { + const cur = symbolToInfo.get(symbol) + if (cur) { + if (dir && !cur.dir) cur.dir = dir + for (const g of gates) if (!cur.declGates.includes(g)) cur.declGates.push(g) + } else { + symbolToInfo.set(symbol, { dir, declGates: gates.slice() }) + } +} + +// Plain `import { X } from './tools//.js'` — no gate. +const importRe = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+'\.\/tools\/([A-Za-z0-9_-]+)\//g +{ + let m: RegExpExecArray | null + while ((m = importRe.exec(toolsTsText)) !== null) { + const dir = m[2] + for (const sym of m[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0]).filter(Boolean)) { + recordSymbol(sym, dir, []) + } + } +} + +// Conditional `const Sym = ? require('./tools//...') : ...`. +// We snip multi-line declarations between `const Sym` and a terminator +// (next blank line, top-level `const`, or `import`/`export`). +const declRe = /^const\s+([A-Za-z0-9_]+)\s*(?::\s*[^=]+)?=\s*([\s\S]*?)(?=\n(?:const|import|export|\s*\/\*|\s*$))/gm +{ + let m: RegExpExecArray | null + while ((m = declRe.exec(toolsTsText)) !== null) { + const sym = m[1] + const body = m[2] + if (!/require\('\.\/tools\//.test(body) && !/require\('\.\/coordinator/.test(body)) continue + // Pick the first `tools/` that the require touches. + const dirMatch = body.match(/require\('\.\/tools\/([A-Za-z0-9_-]+)\//) + const dir = dirMatch ? dirMatch[1] : null + const gates: string[] = [] + for (const fm of body.matchAll(/feature\('([A-Z_]+)'\)/g)) gates.push(fm[1]) + for (const em of body.matchAll(/process\.env\.([A-Z_]+)/g)) gates.push(`env:${em[1]}`) + if (gates.length === 0) { + // No gate detected but require() is unconditional → treat as plain require. + recordSymbol(sym, dir, []) + } else { + recordSymbol(sym, dir, gates) + } + } +} + +// `const getXxxTool = () => require('./tools//...')` (single-expression +// arrow) *and* `const getXxxTool = () => { … require('./tools//...') … }` +// (multi-statement getter, e.g. `getPowerShellTool` at tools.ts:150-155 which +// guards `isPowerShellToolEnabled()` then requires `PowerShellTool.js`) are +// both lazy unconditional imports — the runtime gate, if any, lives either in +// the inline spread inside `getAllBaseTools()` or inside the getter body +// itself. We capture both shapes so the dir mapping survives. The body's +// `isXxxEnabled()` / `process.env.X` checks are picked up later in Phase 2. +const lazyRe = /^const\s+(get[A-Za-z0-9_]+Tool)\s*=\s*\(\)\s*=>\s*(?:\{[\s\S]*?\n\}|[\s\S]*?require\('\.\/tools\/([A-Za-z0-9_-]+)\/[^']*'\))/gm +{ + let m: RegExpExecArray | null + while ((m = lazyRe.exec(toolsTsText)) !== null) { + // Re-scan the matched body for the first `tools/` require so both + // `() => require(...)` and `() => { ... require(...) }` shapes resolve. + const dirMatch = m[0].match(/require\('\.\/tools\/([A-Za-z0-9_-]+)\//) + const dir = dirMatch ? dirMatch[1] : (m[2] ?? null) + recordSymbol(m[1], dir, []) + } +} + +// --------------------------------------------------------------------------- +// Phase 2 · Parse `getAllBaseTools()` body. For each entry we record which +// symbol(s) get added and what *inline* gate (if any) wraps them. Inline +// gates compose with declaration gates. +// --------------------------------------------------------------------------- + +const fnStart = toolsTsText.search(/export function getAllBaseTools\(\)/) +if (fnStart === -1) throw new Error('getAllBaseTools() not found in tools.ts') +// Find `return [` after the function header, then walk balanced brackets to `]`. +const retIdx = toolsTsText.indexOf('return [', fnStart) +if (retIdx === -1) throw new Error('getAllBaseTools(): no `return [` body') +let depth = 1 +let i = retIdx + 'return ['.length +for (; i < toolsTsText.length && depth > 0; i++) { + const ch = toolsTsText[i] + if (ch === '[') depth++ + else if (ch === ']') depth-- +} +if (depth !== 0) throw new Error('getAllBaseTools(): unbalanced return list') +const fnBody = toolsTsText.slice(retIdx + 'return ['.length, i - 1) + +interface InlineEntry { + /** Symbols added by this entry (e.g. `['ConfigTool']` or `['TaskCreateTool', …]`). */ + symbols: string[] + /** Inline gate flags for this entry, e.g. `['env:USER_TYPE', 'fn:isTodoV2Enabled']`. */ + gates: string[] +} + +const inlineEntries: InlineEntry[] = [] + +// Split the body into top-level array entries. Entries are comma-separated +// at depth 0; spreads `...(...)` may carry parens — we track paren/bracket +// depth. +function splitEntries(body: string): string[] { + const out: string[] = [] + let buf = '' + let pdepth = 0 + let bdepth = 0 + for (let k = 0; k < body.length; k++) { + const ch = body[k] + if (ch === '(') pdepth++ + else if (ch === ')') pdepth-- + else if (ch === '[') bdepth++ + else if (ch === ']') bdepth-- + if (ch === ',' && pdepth === 0 && bdepth === 0) { + out.push(buf.trim()) + buf = '' + continue + } + buf += ch + } + if (buf.trim()) out.push(buf.trim()) + return out.filter(Boolean) +} + +for (const raw of splitEntries(fnBody)) { + // Match `...( ? [] : [])` — the canonical conditional spread. + const spread = raw.match(/^\.\.\.\(([\s\S]*?)\?\s*\[([\s\S]*?)\]\s*:\s*\[\s*\]\)$/) + if (spread) { + const gateExpr = spread[1] + const list = spread[2] + const gates: string[] = [] + for (const fm of gateExpr.matchAll(/feature\('([A-Z_]+)'\)/g)) gates.push(fm[1]) + for (const em of gateExpr.matchAll(/process\.env\.([A-Z_]+)/g)) gates.push(`env:${em[1]}`) + for (const fnm of gateExpr.matchAll(/\b(is[A-Z][A-Za-z0-9_]*(?:Enabled|EnabledOptimistic))\(/g)) gates.push(`fn:${fnm[1]}`) + // `getPowerShellTool()`-style getters embed an `isXxxEnabled()` check + // in their body. We surface the getter call itself as a marker so the + // dir is correctly classified as gated even when the inline expression + // doesn't name a flag. + for (const gm of gateExpr.matchAll(/\b(get[A-Z][A-Za-z0-9_]*Tool)\(\)/g)) { + // Look up the getter body in tools.ts and pull out any + // `isXxxEnabled` it references. + const getterRe = new RegExp( + `const\\s+${gm[1]}\\s*=\\s*\\(\\)\\s*=>\\s*\\{[\\s\\S]*?\\}`, + ) + const body = toolsTsText.match(getterRe)?.[0] ?? '' + for (const fnm of body.matchAll(/\b(is[A-Z][A-Za-z0-9_]*(?:Enabled|EnabledOptimistic))\(/g)) gates.push(`fn:${fnm[1]}`) + for (const em of body.matchAll(/process\.env\.([A-Z_]+)/g)) gates.push(`env:${em[1]}`) + // Even if we couldn't extract a named flag, the getter itself is the + // gate; mark it so the dir is feature-gated rather than runtime-leaf. + gates.push(`fn:${gm[1]}`) + } + // Detect bare-symbol truthy gates: the gate expression is just a symbol + // (e.g. `OverflowTestTool ? [OverflowTestTool] : []`). The symbol carries + // its declaration-level gate; we surface it by name so phase 3 can join. + const bareSym = gateExpr.trim().match(/^([A-Za-z_][A-Za-z0-9_]*)$/) + if (bareSym && gates.length === 0) gates.push(`sym:${bareSym[1]}`) + // Accept both bare identifiers (`PowerShellTool`) and getter-call symbols + // (`getPowerShellTool()`). For getters we strip the trailing `()` so the + // resolved name matches what `recordSymbol` registered for the lazy decl + // (Phase 1 `lazyRe`). This is required to register the tool dir + gates + // for entries like `...(getPowerShellTool() ? [getPowerShellTool()] : [])` + // at tools.ts:242 — without it `PowerShellTool` would fall through to the + // "not directly imported" branch and lose `fn:isPowerShellToolEnabled` + // / `fn:getPowerShellTool` evidence (cf. OC-R blocker on + // A.manifest.json:241-260). + const symbols = list + .split(',') + .map(s => s.trim()) + .map(s => { + const callMatch = s.match(/^([A-Za-z_][A-Za-z0-9_]*)\(\)$/) + return callMatch ? callMatch[1] : s + }) + .filter(s => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) + inlineEntries.push({ symbols, gates }) + continue + } + // `...spreadAlias` — e.g. `...cronTools`. The alias carries its own decl gate. + const aliasSpread = raw.match(/^\.\.\.([A-Za-z_][A-Za-z0-9_]*)$/) + if (aliasSpread) { + inlineEntries.push({ symbols: [aliasSpread[1]], gates: [] }) + continue + } + // `getXxxTool() ? [getXxxTool()] : []` style with parens — already covered above. + // Bare entry like `BashTool` or a call like `getSendMessageTool()` → unconditional. + const bareCall = raw.match(/^([A-Za-z_][A-Za-z0-9_]*)\(\)$/) + if (bareCall) { + inlineEntries.push({ symbols: [bareCall[1]], gates: [] }) + continue + } + const bareTok = raw.match(/^([A-Za-z_][A-Za-z0-9_]*)$/) + if (bareTok) { + inlineEntries.push({ symbols: [bareTok[1]], gates: [] }) + continue + } + // Skip anything we can't parse (comments etc.) — they cannot register a tool. +} + +// --------------------------------------------------------------------------- +// Phase 3 · For each tool dir, fold inline + declaration gates together. +// --------------------------------------------------------------------------- + +const dirToGates = new Map>() +const dirIsRegistered = new Set() + +function noteDir(dir: string | null, gates: string[]): void { + if (!dir) return + dirIsRegistered.add(dir) + let s = dirToGates.get(dir) + if (!s) { + s = new Set() + dirToGates.set(dir, s) + } + for (const g of gates) s.add(g) +} + +for (const entry of inlineEntries) { + // Compose effective gates: inline gates ∪ each symbol's decl gates. + const effective = new Set() + for (const g of entry.gates) { + if (g.startsWith('sym:')) { + const refSym = g.slice('sym:'.length) + const info = symbolToInfo.get(refSym) + if (info) for (const dg of info.declGates) effective.add(dg) + // If the symbol is declared with no gate (plain import), `sym:X` falling + // through to no flags would mis-classify as runtime-leaf. Keep the raw + // marker so we still treat it as gated (e.g. `OverflowTestTool` is + // declared with `feature(...)`, but the marker survives even if we + // failed to parse a flag). + effective.add(g) + } else { + effective.add(g) + } + } + for (const sym of entry.symbols) { + const info = symbolToInfo.get(sym) + const dir = info?.dir ?? null + const gates: string[] = Array.from(effective) + if (info) for (const dg of info.declGates) gates.push(dg) + noteDir(dir, gates) + } +} + +// --------------------------------------------------------------------------- +// Phase 4 · Walk `tools/` and emit one ManifestItem per directory + leaf. +// --------------------------------------------------------------------------- + +const dirs = listSubdirs(toolsRoot).filter(d => d !== 'shared' && d !== 'testing') + +const tools: ToolDef[] = [] + +function fileWithLines(rel: string, abs: string): string { + const lines = countLines(abs) + return `${rel}:1-${Math.max(lines, 1)}` +} + +for (const dir of dirs) { + const abs = path.join(toolsRoot, dir) + // Leaf candidates: top-level `*Tool.ts` (or `.tsx`) files inside the dir. + const leafFiles = listFiles(abs, n => /Tool\.tsx?$/.test(n)) + // All TS files under the directory, used for source_files line spans. + const allTs = walkTsFiles(args.source, `tools/${dir}`) + const allTsAnchored = allTs.map(f => `${f.rel}:1-${Math.max(f.lines, 1)}`) + + const gates = Array.from(dirToGates.get(dir) ?? []) + const isRegistered = dirIsRegistered.has(dir) + // `sym:` markers are gate evidence even when their referenced symbol has no + // resolvable flag — drop them from the user-visible flag list once we have + // already used them to classify the dir as feature-gated. + const flags = gates.filter(g => !g.startsWith('sym:')) + + // Family vs leaf: a "family" directory exposes 2+ leaf Tool entrypoint + // files. ScheduleCronTool ships {CronCreate,CronDelete,CronList}Tool.ts. + const familyLeaves = leafFiles.length >= 2 ? leafFiles : [] + + if (familyLeaves.length > 0) { + tools.push({ + name: dir, + category: 'family', + source_files: allTsAnchored, + feature_flags: flags.length ? flags : undefined, + notes: `family of ${familyLeaves.length} leaf tools`, + }) + for (const leafFile of familyLeaves) { + const leafName = leafFile.replace(/\.tsx?$/, '') + const leafRel = `tools/${dir}/${leafFile}` + const leafAbs = path.join(toolsRoot, dir, leafFile) + tools.push({ + name: leafName, + category: gates.length ? 'feature-gated' : 'runtime-leaf', + source_files: [fileWithLines(leafRel, leafAbs)], + feature_flags: flags.length ? flags : undefined, + notes: `leaf of ${dir}`, + }) + } + } else { + let category: ToolDef['category'] + if (gates.length) category = 'feature-gated' + else if (isRegistered) category = 'runtime-leaf' + else category = 'feature-gated' + tools.push({ + name: dir, + category, + source_files: allTsAnchored, + feature_flags: flags.length ? flags : undefined, + notes: isRegistered ? undefined : 'not directly imported by tools.ts (likely indirect)', + }) + } +} + +const items: ManifestItem[] = tools.map(t => ({ + name: t.name, + category: t.category, + source_files: t.source_files, + feature_flags: t.feature_flags, + notes: t.notes, +})) + +const manifest: Manifest = { + generated_at: new Date().toISOString(), + source_commit: sourceCommit, + appendix: 'A', + items, +} + +const counts = { + family: items.filter(i => i.category === 'family').length, + leaf: items.filter(i => i.category === 'runtime-leaf').length, + gated: items.filter(i => i.category === 'feature-gated').length, +} + +const md = [ + '# 附录 A · 工具速查表', + '', + `> source_commit: \`${sourceCommit}\``, + `> generated_at: \`${manifest.generated_at}\``, + `> 共 ${items.length} 项(family=${counts.family} / runtime-leaf=${counts.leaf} / feature-gated=${counts.gated})`, + '', + '本表由 `scripts/gen-tool-table.ts` 扫描 CLI 源码 `tools/` 目录 + `tools.ts` 注册图(含 `getAllBaseTools()` 内联 gate)生成;正文 §C10 引用此表的 leaf 数即可,不要在正文中裸写工具数量。', + '', + '| name | category | feature_flags | source_files |', + '|---|---|---|---|', + ...items.map(i => + `| \`${i.name}\` | ${i.category} | ${(i.feature_flags ?? []).join(', ') || '—'} | ${i.source_files.slice(0, 3).join(', ')}${i.source_files.length > 3 ? ` …(+${i.source_files.length - 3})` : ''} |`, + ), + '', +].join('\n') + +const prev = loadManifest(path.join(args.out, 'A.manifest.json')) +const { manifestPath, tablePath } = writeManifestAndTable(args, 'A', manifest, md) + +if (args.diffSummary) { + process.stdout.write(`appendix-A: ${diffSummary(prev, manifest)}\n`) +} +process.stderr.write(`wrote ${manifestPath} (${items.length} items)\nwrote ${tablePath}\n`) diff --git a/scripts/lib/util.ts b/scripts/lib/util.ts new file mode 100644 index 0000000..636c9b8 --- /dev/null +++ b/scripts/lib/util.ts @@ -0,0 +1,228 @@ +/** + * Shared helpers for V2 appendix manifest generators (附录 A–F) and CI + * lints (`check-source-commits` / `lint-no-fuzzy-quantifiers`). + * + * V2-REVISION-SPEC.md §0.3 requires every manifest to ship with + * - `generated_at`: ISO-8601 timestamp + * - `source_commit`: full git SHA of the CLI source tree + * - `items[]`: canonical-id keyed list of facts + * and §7.4 requires fail-fast diffability between consecutive manifests. + * + * Every script reads the CLI source root via `--source ` (defaults + * to `~/work/code/awesome-project/claude-code-cli` per spec §0.1). + * Run example: + * tsx scripts/gen-tool-table.ts \ + * --source ~/work/code/awesome-project/claude-code-cli \ + * --out docs/appendix + */ + +import { execFileSync } from 'node:child_process' +import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { homedir } from 'node:os' +import * as path from 'node:path' + +export const DEFAULT_SOURCE = path.join( + homedir(), + 'work', + 'code', + 'awesome-project', + 'claude-code-cli', +) + +export const DEFAULT_OUT = path.join('docs', 'appendix') + +export interface CliArgs { + source: string + out: string + sourceCommit?: string + diffSummary: boolean + checkOrphans: boolean + raw: Record +} + +export function parseArgs(argv: string[]): CliArgs { + const raw: Record = {} + for (let i = 2; i < argv.length; i++) { + const tok = argv[i] + if (!tok.startsWith('--')) continue + const key = tok.slice(2) + const next = argv[i + 1] + if (next === undefined || next.startsWith('--')) { + raw[key] = true + } else { + raw[key] = next + i++ + } + } + const source = + typeof raw.source === 'string' + ? raw.source + : process.env.CLI_SOURCE + ? process.env.CLI_SOURCE + : DEFAULT_SOURCE + const out = typeof raw.out === 'string' ? raw.out : DEFAULT_OUT + const sourceCommit = + typeof raw['source-commit'] === 'string' ? raw['source-commit'] : undefined + return { + source, + out, + sourceCommit, + diffSummary: raw['diff-summary'] === true, + checkOrphans: raw['check-orphans'] === true, + raw, + } +} + +/** Resolve the canonical CLI source commit either from --source-commit or `git -C rev-parse HEAD`. */ +export function resolveSourceCommit(args: CliArgs): string { + if (args.sourceCommit) return args.sourceCommit + try { + return execFileSync('git', ['-C', args.source, 'rev-parse', 'HEAD'], { + encoding: 'utf8', + }).trim() + } catch (err) { + throw new Error( + `failed to resolve source_commit at ${args.source}: ${(err as Error).message}`, + ) + } +} + +export function ensureSourceDir(args: CliArgs): void { + if (!existsSync(args.source) || !statSync(args.source).isDirectory()) { + throw new Error( + `source path not found or not a directory: ${args.source} ` + + `(pass --source )`, + ) + } +} + +/** List immediate subdirectory names of `dir`, sorted asc. Excludes `.dot` dirs. */ +export function listSubdirs(dir: string): string[] { + if (!existsSync(dir)) return [] + return readdirSync(dir, { withFileTypes: true }) + .filter(d => d.isDirectory() && !d.name.startsWith('.') && d.name !== 'node_modules') + .map(d => d.name) + .sort() +} + +/** List files matching `predicate` directly inside `dir` (non-recursive), sorted asc. */ +export function listFiles(dir: string, predicate: (name: string) => boolean): string[] { + if (!existsSync(dir)) return [] + return readdirSync(dir, { withFileTypes: true }) + .filter(d => d.isFile() && predicate(d.name)) + .map(d => d.name) + .sort() +} + +export interface FileInfo { + /** Path relative to the CLI source root. */ + rel: string + /** Absolute path on disk. */ + abs: string + /** Line count (LF-counted, last partial line counted). */ + lines: number +} + +/** Recursively collect every `.ts` / `.tsx` file under `dir`, with line counts. */ +export function walkTsFiles(root: string, sub: string): FileInfo[] { + const out: FileInfo[] = [] + const start = path.join(root, sub) + if (!existsSync(start)) return out + const stack: string[] = [start] + while (stack.length) { + const cur = stack.pop()! + for (const entry of readdirSync(cur, { withFileTypes: true })) { + const abs = path.join(cur, entry.name) + if (entry.isDirectory()) { + if (entry.name === '__tests__' || entry.name === 'testing') continue + stack.push(abs) + continue + } + if (!entry.isFile()) continue + if (!/\.(ts|tsx)$/.test(entry.name)) continue + if (/\.test\.(ts|tsx)$/.test(entry.name)) continue + out.push({ + rel: path.relative(root, abs).split(path.sep).join('/'), + abs, + lines: countLines(abs), + }) + } + } + return out.sort((a, b) => a.rel.localeCompare(b.rel)) +} + +export function countLines(abs: string): number { + const text = readFileSync(abs, 'utf8') + if (text.length === 0) return 0 + let n = 1 + for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) n++ + // If file ends with `\n`, the trailing empty line is not a real line. + if (text.charCodeAt(text.length - 1) === 10) n-- + return n +} + +export function readText(abs: string): string { + return readFileSync(abs, 'utf8') +} + +/** Find the first 1-indexed line number where `needle` appears, or `null` if absent. */ +export function findLine(abs: string, needle: string | RegExp): number | null { + const text = readFileSync(abs, 'utf8').split('\n') + for (let i = 0; i < text.length; i++) { + if (typeof needle === 'string') { + if (text[i].includes(needle)) return i + 1 + } else { + if (needle.test(text[i])) return i + 1 + } + } + return null +} + +export interface Manifest { + generated_at: string + source_commit: string + appendix: 'A' | 'B' | 'C' | 'D' | 'E' | 'F' + items: ManifestItem[] +} + +export interface ManifestItem { + name: string + category: string + source_files: string[] + feature_flags?: string[] + wire_type?: string + default_registered?: boolean + notes?: string +} + +export function writeManifestAndTable( + args: CliArgs, + appendix: 'A' | 'B' | 'C' | 'D' | 'E' | 'F', + manifest: Manifest, + markdown: string, +): { manifestPath: string; tablePath: string } { + mkdirSync(args.out, { recursive: true }) + const manifestPath = path.join(args.out, `${appendix}.manifest.json`) + const tablePath = path.join(args.out, `${appendix}.md`) + writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n') + writeFileSync(tablePath, markdown) + return { manifestPath, tablePath } +} + +export function loadManifest(p: string): Manifest | null { + if (!existsSync(p)) return null + return JSON.parse(readFileSync(p, 'utf8')) as Manifest +} + +export function diffSummary(prev: Manifest | null, next: Manifest): string { + if (!prev) return `+${next.items.length} items (initial)` + const prevNames = new Set(prev.items.map(i => i.name)) + const nextNames = new Set(next.items.map(i => i.name)) + const added: string[] = [] + const removed: string[] = [] + for (const n of nextNames) if (!prevNames.has(n)) added.push(n) + for (const n of prevNames) if (!nextNames.has(n)) removed.push(n) + return `+${added.length} -${removed.length}` + + (added.length ? ` added=[${added.slice(0, 8).join(',')}${added.length > 8 ? ',…' : ''}]` : '') + + (removed.length ? ` removed=[${removed.slice(0, 8).join(',')}${removed.length > 8 ? ',…' : ''}]` : '') +} diff --git a/scripts/lint-no-fuzzy-quantifiers.ts b/scripts/lint-no-fuzzy-quantifiers.ts new file mode 100644 index 0000000..3df00ad --- /dev/null +++ b/scripts/lint-no-fuzzy-quantifiers.ts @@ -0,0 +1,130 @@ +/** + * CI lint #2 — `lint-no-fuzzy-quantifiers` (V2-REVISION-SPEC §0.4, §7.4). + * + * Forbidden words inside FACT paragraphs: + * 约 / 大概 / 左右 / 大量 / 不少 / 主要 / 大部分 / 几乎 / 很多 / 一些 + * + * "Fact paragraph" definition (per §0.4): + * - All paragraphs after the chapter's `## 源码锚点` header. + * - EXCLUDING any section whose heading is labeled + * `导言` / `引言` / `前言` / `总结` / `结语` / `小结` / `比喻` + * (these are the §0.4 exception zones — descriptive copy, no facts). + * + * Within a fact paragraph, fenced code blocks (```…```) are excluded — the + * forbidden words can legitimately appear inside source quotations. + * + * Exit 1 with a list of `: ""` on any hit. + */ + +import { existsSync, readdirSync, readFileSync } from 'node:fs' +import * as path from 'node:path' + +const repoRoot = process.cwd() +const docsDir = path.join(repoRoot, 'docs') + +const FORBIDDEN = ['约', '大概', '左右', '大量', '不少', '主要', '大部分', '几乎', '很多', '一些'] + +const EXCEPTION_HEADINGS = ['导言', '引言', '前言', '总结', '结语', '小结', '比喻'] + +interface Hit { + file: string + line: number + text: string + word: string +} + +const hits: Hit[] = [] + +function walk(dir: string): void { + if (!existsSync(dir)) return + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const abs = path.join(dir, entry.name) + if (entry.isDirectory()) { + walk(abs) + continue + } + if (!entry.isFile()) continue + if (!entry.name.endsWith('.md')) continue + // V2-REVISION-SPEC.md itself is a meta-document; skip. + if (entry.name === 'V2-REVISION-SPEC.md') continue + // Appendix tables are auto-generated; skip the canned wording. + if (abs.includes(`${path.sep}appendix${path.sep}`)) continue + scan(abs) + } +} + +function scan(abs: string): void { + const text = readFileSync(abs, 'utf8') + const lines = text.split('\n') + + // Locate `## 源码锚点` — only chapters that opt into §0 公约 are linted. + const anchorIdx = lines.findIndex(l => /^##\s*源码锚点/.test(l)) + if (anchorIdx === -1) return // chapter has not adopted §0.1 yet — skip + + let inException = false + let inFence = false + let exceptionLevel = 0 + + for (let i = anchorIdx + 1; i < lines.length; i++) { + const line = lines[i] + + // Toggle code fence (``` ... ```) + if (/^\s*```/.test(line)) { + inFence = !inFence + continue + } + if (inFence) continue + + // Detect heading transitions. `##` and deeper. + const h = line.match(/^(#{2,6})\s+(.*?)\s*$/) + if (h) { + const level = h[1].length + const title = h[2] + const isException = EXCEPTION_HEADINGS.some(k => title.includes(k)) + if (isException) { + inException = true + exceptionLevel = level + } else if (inException && level <= exceptionLevel) { + // Sibling or higher heading exits the exception zone. + inException = false + } + continue + } + + if (inException) continue + if (!line.trim()) continue + // Skip table separator lines (|---|---|). + if (/^\s*\|[-:|\s]+\|\s*$/.test(line)) continue + + for (const w of FORBIDDEN) { + if (line.includes(w)) { + hits.push({ + file: path.relative(repoRoot, abs), + line: i + 1, + text: line.trim(), + word: w, + }) + } + } + } +} + +walk(docsDir) + +if (hits.length === 0) { + process.stderr.write('lint-no-fuzzy-quantifiers OK — no §0.4 violations in fact paragraphs\n') + process.exit(0) +} + +process.stderr.write( + `lint-no-fuzzy-quantifiers FAIL — ${hits.length} forbidden-word hits (§0.4):\n` + + hits.slice(0, 50).map(h => ` ${h.file}:${h.line} 「${h.word}」 ${truncate(h.text, 120)}\n`).join('') + + (hits.length > 50 ? ` …(+${hits.length - 50} more)\n` : '') + + `\nMove the wording to an 导言/总结/比喻 sub-section or replace with a precise number citing the appendix manifest.\n`, +) +process.exit(1) + +function truncate(s: string, max: number): string { + if (s.length <= max) return s + return s.slice(0, max - 1) + '…' +} diff --git a/scripts/orphan-allowlist.txt b/scripts/orphan-allowlist.txt new file mode 100644 index 0000000..a226d73 --- /dev/null +++ b/scripts/orphan-allowlist.txt @@ -0,0 +1,21 @@ +# Orphan allowlist for `gen-module-matrix.ts --check-orphans` (V2-REVISION-SPEC §7.6). +# +# One top-level CLI source entry per line. Lines starting with `#` are +# comments. Each allowlisted entry MUST carry a comment explaining why no +# v2 chapter claims it. +# +# Path is the entry name relative to the CLI source root, e.g. `cli` (dir) +# or `setup.ts` (file). + +# `cli/` — runtime CLI plumbing (transports, REPL launchers). C23 covers +# `cli/transports/` explicitly; the rest of `cli/` is mechanical entry +# wiring already accounted for under C02 (启动链路). +cli + +# `plugins/` — plugin registry & bundled plugin payloads. C21 covers the +# plugin extension surface; the bundled payload tree is a fixture, not a +# concept chapter. +plugins + +# `types/` — shared TypeScript type aliases (no runtime concept). +types diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a9c2a25 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "allowImportingTsExtensions": false, + "resolveJsonModule": true + }, + "include": ["scripts/**/*.ts"] +}