diff --git a/external_plugins/advisor-mode-for-zcode/.zcode-plugin/plugin.json b/external_plugins/advisor-mode-for-zcode/.zcode-plugin/plugin.json new file mode 100644 index 0000000..6d71eb1 --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/.zcode-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "name": "advisor-mode-for-zcode", + "version": "0.1.0", + "description": "Dual-tier advisor and executor workflow for ZCode with hard lifecycle gates, minimalist engineering, clean prose, and action-first ergonomics.", + "author": { + "name": "Gianluca Iannotta", + "url": "https://github.com/jannotix" + }, + "homepage": "https://github.com/jannotix/advisor-mode-for-zcode", + "repository": "https://github.com/jannotix/advisor-mode-for-zcode", + "license": "Apache-2.0", + "keywords": [ + "zcode", + "advisor-mode", + "subagents", + "code-review", + "yagni", + "minimalist-engineering" + ], + "skills": "./skills", + "hooks": "./hooks/hooks.json", + "description_i18n": { + "en": "Dual-tier advisor and executor workflow for ZCode with hard lifecycle gates, minimalist engineering, clean prose, and action-first ergonomics.", + "zh-CN": "面向 ZCode 的双层工程工作流:前沿顾问负责审阅,跟随会话模型的快速执行者负责实施,并以硬性生命周期门禁、极简工程、洁净文风与行动优先工效加以约束。" + } +} diff --git a/external_plugins/advisor-mode-for-zcode/LICENSE b/external_plugins/advisor-mode-for-zcode/LICENSE new file mode 100644 index 0000000..08903b8 --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Gianluca Iannotta + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/external_plugins/advisor-mode-for-zcode/NOTICE b/external_plugins/advisor-mode-for-zcode/NOTICE new file mode 100644 index 0000000..1da2f2b --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/NOTICE @@ -0,0 +1,10 @@ +Advisor Mode for Zcode +Copyright 2026 Gianluca Iannotta + +This product includes software developed by Gianluca Iannotta (https://github.com/jannotix). + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/external_plugins/advisor-mode-for-zcode/README.md b/external_plugins/advisor-mode-for-zcode/README.md new file mode 100644 index 0000000..8fb8fe3 --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/README.md @@ -0,0 +1,128 @@ +# Advisor Mode for Zcode + +[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +A dual-tier engineering workflow for ZCode. Advisor Mode pairs a high-reasoning frontier Advisor and Reviewer with a fast Executor that follows the session model, guided by built-in minimalist engineering, clean prose, action-first communication, and UI craft standards. Hard lifecycle gates enforce the workflow at the tool level, so the discipline holds even when the session model forgets it. + +## Architecture + +The plugin classifies the session at startup from the session model id and switches behavior accordingly. + +### 1. Executor Session (Fast Model) + +When the session runs on the fast model tier, the session acts as the Executor and must consult the `advisor-reviewer` subagent (frontier model) at three mandatory gates: + +- **Architecture, Planning and Pre-Task Gate**: fires when planning or designing a product, system, architecture, specification, or roadmap, and before multi-file or sensitive edits. The hook denies a second distinct file edit until the advisor validates the plan. +- **Two-Strike Circuit Breaker**: fires when the same command fails twice with a normalized identical signature (timestamps, hashes, and counts ignored). Commands and edits stay blocked until an advisor consultation releases the breaker. +- **Mandatory Final Task Review Gate**: before the task can stop, every modified file needs an approved advisor review whose recorded SHA-256 hash still matches the file on disk. The Stop hook forces the session back to work (up to twice), then warns explicitly if the review is still missing. + +### 2. Advisor Session (Frontier Model) + +When the session runs on the frontier model tier, the session acts directly as Architect, Advisor, and Final Reviewer. It delegates implementation to the `flash-executor` subagent and audits the diff and the command exit codes before closing the task. + +## Model Selection and Future Models + +- `flash-executor` uses `model: inherit`, so it always runs on whatever model the user selected for the session. Switching the session model is the only step needed to move the executor to a new release. +- `advisor-reviewer` pins the frontier model on a single `model:` line of its agent file, because ZCode resolves subagent models from the agent definition. The line carries an explicit reasoning level through the `$` suffix (for example `model: account:/GLM-5.3$max`); ZCode rejects a pinned reasoning-capable model without one. When a new frontier model is released, that one line is the whole migration. +- Role detection matches the session model id against configurable patterns in `advisor-config.json` (executor patterns first, unknown models fail closed to executor). + +## Built-In Craft Standards + +The plugin injects four non-negotiable disciplines into every session: + +1. **Minimalist Engineering**: comprehension before brevity, the 7-rung simplicity ladder, surgical edits, root-cause fixes, and failing-first acceptance checks. +2. **Clean Prose**: active voice, concrete subjects, no filler phrases, and zero em dashes. +3. **Action-First Cognitive Ergonomics**: the answer or action on line 1, bounded steps, errors reported strictly as cause and fix. +4. **UI Direction**: local brand tokens and palettes first, no unapproved CDN assets. + +## Installation + +Requirements: ZCode with plugin support and a `node` runtime on the PATH (the hooks run a small Node script). + +Install from the ZCode plugin marketplace. After installation, start a new session: the SessionStart hook announces the detected role and the active gates. To run the fast-tier executor workflow, select a flash session model; to run as the architect, select a frontier session model. + +## Configuration + +Settings live in `advisor-config.json` inside the installed plugin directory: + +```json +{ + "enabled": true, + "roles": { + "unknownModelRole": "executor", + "advisor": { + "label": "Frontier Advisor", + "agentName": "advisor-reviewer", + "uiModelPatterns": ["glm-5.3"] + }, + "executor": { + "label": "Fast Executor", + "agentName": "flash-executor", + "uiModelPatterns": ["flash"] + } + }, + "gates": { + "planBeforeComplexTask": true, + "maxConsecutiveFailures": 2, + "mandatoryFinalReview": true, + "maxStopContinuations": 2 + }, + "defaults": { + "simplicityIntensity": "full", + "actionFirstOutput": true + } +} +``` + +## Side Effects and Dependencies + +- Runtime requirement: a `node` executable on the PATH. Six lifecycle hooks run a small Node script (10 second timeout each); they read a JSON payload on stdin and write state under the ZCode plugin data directory. +- File writes: per-session state files and an error log inside the plugin data directory only. The hooks never write into project files. +- Command execution: the `advisor-reviewer` agent may run read-only verification commands such as `git status` and `git diff`; the `flash-executor` agent runs the commands needed by its task. +- Model and service dependencies: uses the ZCode session model and one pinned frontier model resolved through the user's own configured Z.ai account. No other network access, no MCP servers, no third-party code, assets, or services. + +## Lifecycle Hooks + +The plugin registers hooks on six ZCode events: + +| Event | Purpose | +| --- | --- | +| `SessionStart` | Detects the session role from the model id and injects gate directives plus craft standards | +| `UserPromptSubmit` | Injects a warning while the circuit breaker is active | +| `PreToolUse` | Denies commands and edits while the breaker is tripped; denies multi-file edits without an approved plan | +| `PostToolUse` | Tracks failure signatures, records file hashes, captures advisor verdicts | +| `PostToolUseFailure` | Counts command failures reported through the failure event | +| `Stop` | Enforces the mandatory final review with hash verification, forces continuation when missing | + +## Plugin Structure + +- `.zcode-plugin/plugin.json`: plugin manifest. +- `hooks/hooks.json`: hook definitions. +- `scripts/advisor-hook.mjs`: cross-platform Node.js lifecycle hook runner and gate validator. +- `tests/advisor-hook.test.mjs`: automated test suite covering roles, gates, signatures, and hash verification. +- `rules/craft.md`: craft standards injected at session start. +- `agents/advisor-reviewer.md`: frontier advisor, architect, and reviewer agent. +- `agents/flash-executor.md`: fast executor agent. +- `skills/advisor-mode/SKILL.md`: configuration skill. + +## Testing the Hooks + +```bash +# Run the unit and integration test suite +node --test tests/advisor-hook.test.mjs + +# Dry run a session start hook (payload on stdin) +echo '{"session_id":"demo","source":"startup","model":"your-session-model-id"}' | node ./scripts/advisor-hook.mjs SessionStart +``` + +## Author and Developer + +Created and maintained by **Gianluca Iannotta**. Source repository: https://github.com/jannotix/advisor-mode-for-zcode + +## License + +This project is licensed under the Apache License, Version 2.0. See the [LICENSE](LICENSE) and [NOTICE](NOTICE) files for details. + +## Disclaimer + +Advisor Mode for Zcode is an independent open-source project created and maintained by Gianluca Iannotta. It is not affiliated with, endorsed by, sponsored by, or officially connected to Z.ai or the ZCode team. All product and company names are trademarks or registered trademarks of their respective holders; their use does not imply any affiliation with or endorsement by them. diff --git a/external_plugins/advisor-mode-for-zcode/README_CN.md b/external_plugins/advisor-mode-for-zcode/README_CN.md new file mode 100644 index 0000000..73ba936 --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/README_CN.md @@ -0,0 +1,128 @@ +# Advisor Mode for Zcode + +[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + +面向 ZCode 的双层工程工作流。Advisor Mode 将高推理能力的前沿顾问与审阅者,和跟随会话模型的快速执行者配对使用,并内建极简工程、洁净文风、行动优先沟通与界面工艺标准。硬性生命周期门禁在工具层强制执行该工作流,即使会话模型忘记规则,纪律依然有效。 + +## 架构 + +插件在会话启动时根据会话模型标识判定角色,并据此切换行为。 + +### 1. 执行者会话(快速模型) + +当会话运行在快速模型层级时,会话充当执行者,并必须在三个强制门禁处咨询 `advisor-reviewer` 子代理(前沿模型): + +- **架构、规划与任务前门禁**:在规划或设计产品、系统、架构、规格或路线图时触发,也在多文件或敏感编辑之前触发。在顾问验证计划之前,钩子会拒绝第二个不同文件的编辑。 +- **两连败熔断器**:同一条命令以归一化的相同签名失败两次时触发(忽略时间戳、哈希和计数)。在咨询顾问之前,命令与编辑保持封锁。 +- **强制最终审阅门禁**:任务可以停止之前,每个被修改的文件都需要一份已批准的顾问审阅,其记录的 SHA-256 哈希仍与磁盘上的文件一致。Stop 钩子最多两次强制会话返回工作,之后若审阅仍缺失则明确警告。 + +### 2. 顾问会话(前沿模型) + +当会话运行在前沿模型层级时,会话直接充当架构师、顾问和最终审阅者。它把实现工作委托给 `flash-executor` 子代理,并在关闭任务之前审计差异和命令退出码。 + +## 模型选择与未来模型 + +- `flash-executor` 使用 `model: inherit`,因此始终运行在用户为会话选择的模型上。切换会话模型是让执行者迁移到新版本所需的唯一步骤。 +- `advisor-reviewer` 在其代理文件的单行 `model:` 上固定前沿模型,因为 ZCode 从代理定义解析子代理模型。该行通过 `$` 后缀携带显式推理档位(例如 `model: account:/GLM-5.3$max`);ZCode 会拒绝缺少推理档位的固定推理模型。新前沿模型发布时,这一行就是全部迁移工作。 +- 角色检测将会话模型标识与 `advisor-config.json` 中可配置的模式匹配(先检查执行者模式,未知模型回退为执行者)。 + +## 内建工艺标准 + +插件向每个会话注入四条不可协商的纪律: + +1. **极简工程**:先理解后精简、七级简化阶梯、外科手术式编辑、根因修复、先失败后通过的验收检查。 +2. **洁净文风**:主动语态、具体主语、无填充短语、零长划线。 +3. **行动优先认知工效**:第一行给出答案或行动、有界步骤、错误严格按原因与修复报告。 +4. **界面方向**:优先本地品牌令牌与调色板,不加载未经批准的 CDN 资源。 + +## 安装 + +要求:支持插件的 ZCode,以及 PATH 上可用的 `node` 运行时(钩子运行一个小型 Node 脚本)。 + +从 ZCode 插件市场安装。安装后开始新会话:SessionStart 钩子会通告检测到的角色和已启用的门禁。要运行快速层执行者工作流,请选择 flash 会话模型;要以架构师身份运行,请选择前沿会话模型。 + +## 配置 + +设置保存在已安装插件目录内的 `advisor-config.json`: + +```json +{ + "enabled": true, + "roles": { + "unknownModelRole": "executor", + "advisor": { + "label": "Frontier Advisor", + "agentName": "advisor-reviewer", + "uiModelPatterns": ["glm-5.3"] + }, + "executor": { + "label": "Fast Executor", + "agentName": "flash-executor", + "uiModelPatterns": ["flash"] + } + }, + "gates": { + "planBeforeComplexTask": true, + "maxConsecutiveFailures": 2, + "mandatoryFinalReview": true, + "maxStopContinuations": 2 + }, + "defaults": { + "simplicityIntensity": "full", + "actionFirstOutput": true + } +} +``` + +## 副作用与依赖 + +- 运行时要求:PATH 上可用的 `node` 可执行文件。六个生命周期钩子各运行一个小型 Node 脚本(每个 10 秒超时);它们从 stdin 读取 JSON 负载,并把状态写入 ZCode 插件数据目录。 +- 文件写入:仅在插件数据目录内写入按会话的状态文件和错误日志。钩子绝不写入项目文件。 +- 命令执行:`advisor-reviewer` 代理可以运行只读验证命令,例如 `git status` 和 `git diff`;`flash-executor` 代理运行其任务所需的命令。 +- 模型与服务依赖:使用 ZCode 会话模型,以及通过用户自己配置的 Z.ai 账户解析的一个固定前沿模型。无其他网络访问,无 MCP 服务器,无第三方代码、资产或服务。 + +## 生命周期钩子 + +插件在六个 ZCode 事件上注册钩子: + +| 事件 | 用途 | +| --- | --- | +| `SessionStart` | 从模型标识检测会话角色,注入门禁指令与工艺标准 | +| `UserPromptSubmit` | 熔断器激活期间注入警告 | +| `PreToolUse` | 熔断器触发期间拒绝命令与编辑;无已批准计划时拒绝多文件编辑 | +| `PostToolUse` | 跟踪失败签名、记录文件哈希、捕获顾问裁决 | +| `PostToolUseFailure` | 统计通过失败事件上报的命令失败 | +| `Stop` | 以哈希验证强制最终审阅,缺失时强制继续 | + +## 插件结构 + +- `.zcode-plugin/plugin.json`:插件清单。 +- `hooks/hooks.json`:钩子定义。 +- `scripts/advisor-hook.mjs`:跨平台 Node.js 生命周期钩子运行器与门禁验证器。 +- `tests/advisor-hook.test.mjs`:覆盖角色、门禁、签名与哈希验证的自动化测试套件。 +- `rules/craft.md`:会话启动时注入的工艺标准。 +- `agents/advisor-reviewer.md`:前沿顾问、架构师与审阅者代理。 +- `agents/flash-executor.md`:快速执行者代理。 +- `skills/advisor-mode/SKILL.md`:配置技能。 + +## 测试钩子 + +```bash +# 运行单元与集成测试套件 +node --test tests/advisor-hook.test.mjs + +# 干运行会话启动钩子(stdin 传入负载) +echo '{"session_id":"demo","source":"startup","model":"your-session-model-id"}' | node ./scripts/advisor-hook.mjs SessionStart +``` + +## 作者与开发者 + +由 **Gianluca Iannotta** 创建并维护。源码仓库:https://github.com/jannotix/advisor-mode-for-zcode + +## 许可证 + +本项目基于 Apache License, Version 2.0 授权。详情见 [LICENSE](LICENSE) 与 [NOTICE](NOTICE) 文件。 + +## 免责声明 + +Advisor Mode for Zcode 是由 Gianluca Iannotta 创建并维护的独立开源项目。它与 Z.ai 及 ZCode 团队无隶属、背书、赞助或官方关联。所有产品与公司名称均为其各自持有者的商标或注册商标;它们的使用不表示任何隶属或背书。 diff --git a/external_plugins/advisor-mode-for-zcode/advisor-config.json b/external_plugins/advisor-mode-for-zcode/advisor-config.json new file mode 100644 index 0000000..de0dd82 --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/advisor-config.json @@ -0,0 +1,26 @@ +{ + "enabled": true, + "roles": { + "unknownModelRole": "executor", + "advisor": { + "label": "Frontier Advisor", + "agentName": "advisor-reviewer", + "uiModelPatterns": ["glm-5.3"] + }, + "executor": { + "label": "Fast Executor", + "agentName": "flash-executor", + "uiModelPatterns": ["flash"] + } + }, + "gates": { + "planBeforeComplexTask": true, + "maxConsecutiveFailures": 2, + "mandatoryFinalReview": true, + "maxStopContinuations": 2 + }, + "defaults": { + "simplicityIntensity": "full", + "actionFirstOutput": true + } +} diff --git a/external_plugins/advisor-mode-for-zcode/agents/advisor-reviewer.md b/external_plugins/advisor-mode-for-zcode/agents/advisor-reviewer.md new file mode 100644 index 0000000..9b1aa9d --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/agents/advisor-reviewer.md @@ -0,0 +1,63 @@ +--- +name: advisor-reviewer +description: Frontier advisor, architect, and mandatory final task reviewer. Invoke when planning or designing a product, system, architecture, specification, or roadmap (plan gate), before multi-file or complex implementation tasks, after two consecutive identical command failures (circuit breaker), and at the end of every executor task (final review gate). +tools: Read, Glob, Grep, Bash +model: account:zai-individual-coding-plan/GLM-5.3$max +--- + +# Advisor, Architect and Final Reviewer (`advisor-reviewer`) + +You are the frontier Advisor, Architect, and Final Task Reviewer. You operate strictly as a high-reasoning consultant and gatekeeper: you never edit files directly (Write and Edit are disabled). You read code, inspect diffs, run read-only verification commands such as `git status` and `git diff` when needed, and return surgical, high-signal direction or review verdicts. + +## Operating Modes + +Determine which gate triggered your invocation from the caller's prompt. + +### Gate 1: Architecture, Planning and Pre-Task Gate + +1. Triggered when the user asks to plan or design a product, system, architecture, specification, or roadmap (even before code is written), or before editing multiple files or starting any non-trivial implementation. +2. Read the target files, the requirements, and the relevant workspace instructions. +3. Apply the 7-rung simplicity ladder (skip speculative needs, reuse the codebase, standard library, native platform features, existing dependencies, one line, minimal code). No unrequested abstractions, no single-implementation interfaces, no single-product factories, no configuration for constants. +4. Verify that the proposed plan defines concrete trust-boundary validations and runnable checks or tests that can fail before code is written. +5. Return a numbered execution plan with at most 5 steps, naming the exact files and functions to touch, the trust-boundary validations required, and which speculative complexity to skip. + +### Gate 2: Two-Strike Circuit Breaker (Stuck Executor) + +1. Inspect the exact command or test failure and the code path involved. +2. Identify the root cause. Never patch symptoms at one caller when sibling callers share the bug. +3. Return exactly two lines: + - `Cause:` the root cause with file and line numbers. + - `Fix:` the minimal change that resolves the root cause, or `STOP_FOR_USER` when the blocker needs an owner decision or changes scope. + +### Gate 3: Mandatory Final Task Review (Post-Executor Gate) + +Before any executor task is accepted as done, audit the work across two sections. + +#### Section 1: Correctness, Security and Evidence Gate + +- Scope and surgical diff: does every changed line trace to the approved task? Were adjacent lines, formatting, or unrelated code modified? Were orphaned imports and functions cleaned up? +- Security and trust boundaries: are inputs validated (fail closed), outputs escaped, SQL queries bound, secrets kept out of code and commits, and attribution notices absent? +- Verification evidence: did the executor run the required checks and report the exact commands and exit codes? Run `git status` and `git diff` yourself to verify when useful. + +#### Section 2: Complexity and Prose Audit + +Scan the diff for unnecessary complexity and machine slop. List one line per finding in this exact shape: + +`:L: . .` + +Tags: +- `delete:` dead code, unused flexibility, speculative feature. +- `stdlib:` hand-rolled logic covered by the standard library. +- `native:` custom code doing what the platform already provides. +- `yagni:` single-implementation interface, single-caller wrapper, unrequested configuration. +- `shrink:` same logic in fewer lines. +- `slop:` passive voice, filler prose, or banned em dashes. + +End Section 2 with `net: - lines possible.` or `Lean already. Ship.` + +#### Final Verdict + +Conclude with one line, using one of these exact formats so the lifecycle gate can verify the outcome: + +- `VERDICT: APPROVED` (only when all checks pass, zero security or scope defects exist, and no required cuts remain). +- `VERDICT: CHANGES_REQUIRED` followed by a numbered list of the exact fixes the executor must apply before reporting to the user. diff --git a/external_plugins/advisor-mode-for-zcode/agents/flash-executor.md b/external_plugins/advisor-mode-for-zcode/agents/flash-executor.md new file mode 100644 index 0000000..cbbd250 --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/agents/flash-executor.md @@ -0,0 +1,28 @@ +--- +name: flash-executor +description: Fast executor subagent that follows the session model. Implement one approved task at a time, run the checks, and report every executed command with its exit code. +tools: Read, Write, Edit, Glob, Grep, Bash +model: inherit +--- + +# Fast Executor (`flash-executor`) + +You are the Executor and you run on the session model, so you always follow the model the user selected for the conversation. You implement one approved task at a time with surgical precision, speed, and zero over-engineering. + +## Execution Rules + +1. **Read first**: read the files named in the task prompt and trace the code flow before editing. Honor every workspace instruction file the task references. +2. **Stop on ambiguity**: you never change scope and never guess on ambiguous requirements. When something is unclear, stop immediately and return `BLOCKED_UNCLEAR:` naming the exact question for the advisor. +3. **7-rung simplicity**: write the least code that works. Reuse existing codebase helpers, the standard library, and native platform mechanisms first. No unrequested abstractions, no single-implementation interfaces, no speculative configuration, no files over 400 lines. Validate all inputs at trust boundaries and fail closed. +4. **Surgical edits**: touch only the lines the task requires. Leave adjacent code, comments, and formatting untouched; list unrelated issues under out-of-scope findings. Remove only imports or helpers orphaned by your own edit. +5. **Goal-driven checks and the two-strike limit**: + - Define or run the check or test that fails before the fix and passes after. + - When a command or test fails twice in the same way, STOP IMMEDIATELY. Never attempt a third blind retry. Return `BLOCKED_TWO_STRIKES:` with the command, the exit code, and the stderr or stdout so the advisor can diagnose the root cause. +6. **Attribution cleanliness and clean prose**: never add generator attribution in code, comments, or commit messages. Write files in English unless instructed otherwise, with active voice and zero em dashes. + +## Required Return Report (in the user's conversation language) + +1. **Status**: `COMPLETED` or `BLOCKED_TWO_STRIKES` or `BLOCKED_UNCLEAR`. +2. **Modified files**: list of paths with a one-line summary per file. +3. **Executed commands**: every command run with its exact exit code, in the shape ` -> exit `. +4. **Out-of-scope findings**: any deprecated API, dead code, or adjacent bug noticed but left untouched. diff --git a/external_plugins/advisor-mode-for-zcode/hooks/hooks.json b/external_plugins/advisor-mode-for-zcode/hooks/hooks.json new file mode 100644 index 0000000..b8e584f --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/hooks/hooks.json @@ -0,0 +1,79 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": ["${ZCODE_PLUGIN_ROOT}/scripts/advisor-hook.mjs", "SessionStart"], + "timeoutMs": 10000 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": ["${ZCODE_PLUGIN_ROOT}/scripts/advisor-hook.mjs", "UserPromptSubmit"], + "timeoutMs": 10000 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "^(Bash|Write|Edit|ApplyPatch)$", + "hooks": [ + { + "type": "process", + "command": "node", + "args": ["${ZCODE_PLUGIN_ROOT}/scripts/advisor-hook.mjs", "PreToolUse"], + "timeoutMs": 10000 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "^(Bash|Write|Edit|ApplyPatch|Agent|Task)$", + "hooks": [ + { + "type": "process", + "command": "node", + "args": ["${ZCODE_PLUGIN_ROOT}/scripts/advisor-hook.mjs", "PostToolUse"], + "timeoutMs": 10000 + } + ] + } + ], + "PostToolUseFailure": [ + { + "matcher": "^Bash$", + "hooks": [ + { + "type": "process", + "command": "node", + "args": ["${ZCODE_PLUGIN_ROOT}/scripts/advisor-hook.mjs", "PostToolUseFailure"], + "timeoutMs": 10000 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": ["${ZCODE_PLUGIN_ROOT}/scripts/advisor-hook.mjs", "Stop"], + "timeoutMs": 10000 + } + ] + } + ] + } +} diff --git a/external_plugins/advisor-mode-for-zcode/rules/craft.md b/external_plugins/advisor-mode-for-zcode/rules/craft.md new file mode 100644 index 0000000..85e410b --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/rules/craft.md @@ -0,0 +1,45 @@ +# Craft Standards (always active) + +## Authorship and attribution hygiene + +- The user is the sole author of all work. Never add generator notices, co-author trailers, robot emojis, or tool and model signatures in commits, pull requests, release notes, changelogs, code comments, documentation, or metadata. Technical identifiers the product requires stay valid. +- Never read real credential files and never place secrets in files, prompts, logs, or commits. + +## A. Minimalist engineering and complexity audit + +- Comprehension before brevity: read every file the change touches and trace the real flow end to end before choosing a solution. State assumptions up front. When multiple readings or a simpler approach exist, present the options and let the user decide. +- The 7-rung ladder, stopping at the first rung that holds: + 1. Does it need to exist at all? Skip speculative needs and state why in one line. + 2. Already in this codebase? Reuse existing helpers, types, and patterns. + 3. Standard library covers it? Use the standard library. + 4. Native platform feature covers it? Use the native framework, platform, HTML, CSS, or database constraints before custom code. + 5. An installed dependency covers it? Use it; never add a dependency for what a few lines can do. + 6. Can it be one line? Write one line. + 7. Only then: write the minimum code that works. +- No unrequested abstractions: no single-implementation interfaces, no single-product factories, no speculative abstractions, no configuration flags for constants. +- Surgical changes and root-cause fixes: fix root causes in shared functions rather than patching symptoms across callers. Every changed line must trace to the approved task. Leave adjacent code, comments, and formatting alone; report unrelated dead code or deprecated APIs as findings instead of editing them. Remove only imports, variables, or functions orphaned by your own change. Keep files under 400 lines. +- Non-negotiable floors: never simplify away input validation at trust boundaries (always fail closed), security checks, licensing gates, or data-loss prevention. Every non-trivial branch, parser, monetary path, or security path requires a runnable check that fails before the fix and passes after. Mark deliberate simplifications that have a known scaling ceiling with a comment in the shape `ceiling: , upgrade: `. +- Complexity review tags for diffs: `delete:` dead code, unused flexibility, speculative feature; `stdlib:` hand-rolled logic covered by the standard library; `native:` custom code doing what the platform already provides; `yagni:` single-implementation interface, single-caller wrapper, unrequested configuration; `shrink:` same logic in fewer lines. Conclude the audit with `net: - lines possible.` or `Lean already. Ship.` + +## B. Clean prose standard + +- Talk to the user in their preferred conversation language. Write code, identifiers, comments, and project files in English unless project rules say otherwise. +- Use active voice with a concrete human or system subject. Cut throat-clearing openers, filler phrases, hedging adverbs, and pull-quote drama. +- Zero em dashes: the long dash character is banned across all prose and documentation. +- Avoid binary rhetorical contrasts in the shape "not X, it is Y". State the specific fact directly. + +## C. Action-first cognitive ergonomics + +- Lead with the action or answer: the first line of every reply must be the concrete next action, command, path, or direct answer. Never open with conversational preambles and never close with pleasantries or post-task recaps. +- Bound multi-step tasks to at most 5 numbered items per visible group, one bounded action per step, and restate current step progress at the start of each turn. +- Finish the current issue before addressing secondary items; offer secondary findings at the end. +- Report errors strictly as `Cause:` and `Fix:` with file, line, and exit code. +- Give concrete estimates in minutes, hours, or sessions. +- Ask when a decision is needed and always ask before any destructive action. + +## D. UI and frontend direction + +- Brand truth first: use the project's local design tokens, style definitions, palettes, typography, and asset directories for all colors, fonts, logos, and icons. Never invent arbitrary styles and never load fonts or scripts from unapproved third-party CDNs. +- Match the design to the surface purpose: Persuade (landing and marketing), Operate (dashboards and tools), Read (documentation and legal), or Experience (portfolios and showcases). +- Prefer refinement over redesign unless the user asks for a redesign: refinement preserves the incumbent look. +- Bound verification passes: build the complete surface, inspect desktop and mobile together in one pass, apply fixes in one batch, confirm once, and stop. diff --git a/external_plugins/advisor-mode-for-zcode/scripts/advisor-hook.mjs b/external_plugins/advisor-mode-for-zcode/scripts/advisor-hook.mjs new file mode 100644 index 0000000..e1d5137 --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/scripts/advisor-hook.mjs @@ -0,0 +1,457 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import crypto from 'node:crypto'; + +const FILE_EDIT_TOOLS = new Set(['Write', 'Edit', 'ApplyPatch', 'MultiEdit', 'write_file', 'edit_file']); +const COMMAND_TOOLS = new Set(['Bash', 'run_command']); +const AGENT_TOOLS = new Set(['Agent', 'Task', 'invoke_subagent']); + +function extractTargetFile(toolInput) { + return toolInput?.file_path || toolInput?.filePath || toolInput?.path || toolInput?.Path || toolInput?.notebook_path || ''; +} + +function extractCommand(toolInput) { + return String(toolInput?.command || toolInput?.CommandLine || '').trim().toLowerCase(); +} + +function sha256Hex(data) { + return crypto.createHash('sha256').update(data).digest('hex'); +} + +function hashFileOnDisk(filePath, fsImpl) { + try { + if (filePath && fsImpl?.existsSync(filePath)) return sha256Hex(fsImpl.readFileSync(filePath)); + } catch {} + return ''; +} + +function stringifyResponse(value) { + if (value == null) return ''; + if (typeof value === 'string') return value; + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function responseFailureExitCode(toolResponse) { + if (!toolResponse || typeof toolResponse !== 'object') return 0; + if (typeof toolResponse.exitCode === 'number') return toolResponse.exitCode; + if (typeof toolResponse.exit_code === 'number') return toolResponse.exit_code; + const nested = toolResponse.content ?? toolResponse.result; + if (Array.isArray(nested)) { + for (const part of nested) { + if (part && typeof part === 'object') { + if (typeof part.exitCode === 'number') return part.exitCode; + if (typeof part.exit_code === 'number') return part.exit_code; + } + } + } + return 0; +} + +function isCommandFailure(payload) { + if (payload?.error && String(payload.error).trim()) return true; + if (typeof payload?.exitCode === 'number' && payload.exitCode !== 0) return true; + const tr = payload?.tool_response ?? payload?.toolResult ?? payload?.toolResultContent; + if (tr && typeof tr === 'object') { + if (tr.error && String(tr.error).trim()) return true; + if (tr.is_error === true || tr.isError === true) return true; + if (responseFailureExitCode(tr) !== 0) return true; + if (tr.status === 'ERROR' || tr.status === 'error') return true; + const nested = tr.content ?? tr.result; + if (Array.isArray(nested)) { + for (const part of nested) { + if (part && typeof part === 'object' && (part.is_error === true || part.isError === true)) return true; + } + } + } + return false; +} + +function failureErrorText(payload) { + const tr = payload?.tool_response ?? payload?.toolResult; + if (tr && typeof tr === 'object') { + const nestedErr = tr.stderr || tr.error || tr.output || ''; + if (String(nestedErr).trim()) return String(nestedErr); + } + if (typeof tr === 'string' && tr.trim()) return tr; + return String(payload?.error || payload?.error_details?.message || `exit code ${payload?.exitCode ?? 1}`); +} + +function normalizeFailureSignature(payload) { + const toolInput = payload?.tool_input ?? payload?.toolInput ?? {}; + const cmd = extractCommand(toolInput); + const tr = payload?.tool_response ?? payload?.toolResult; + const exitCode = isCommandFailure(payload) ? (responseFailureExitCode(tr) || payload?.exitCode || 1) : 0; + const rawErr = failureErrorText(payload).toLowerCase(); + const normErr = rawErr + .replace(/\d{4}-\d{2}-\d{2}[t\s]\d{2}:\d{2}:\d{2}[^\s]*/gi, '') + .replace(/[a-f0-9]{8,}/gi, '') + .replace(/\b\d+\b/g, '') + .replace(/\s+/g, ' ') + .trim(); + return `${cmd}::${exitCode}::${normErr}`; +} + +function loadConfig(pluginDir, fsImpl) { + try { + const configPath = path.join(pluginDir, 'advisor-config.json'); + if (!fsImpl.existsSync(configPath)) return null; + const cfg = JSON.parse(fsImpl.readFileSync(configPath, 'utf8')); + if (!cfg || cfg.enabled === false) return null; + return cfg; + } catch { + return null; + } +} + +function freshState(role) { + return { + role: role || '', + modifiedFiles: {}, + verifiedFiles: {}, + circuitBreakerTripped: false, + circuitBreakerReason: '', + consecutiveFailures: 0, + sameErrorFailures: 0, + lastError: '', + lastErrorSignature: '', + planValidatedByAdvisor: false, + needsFinalReview: false, + reviewStatus: '', + lastReview: null, + stopContinuations: 0, + taskStatus: '', + sessionStartedAt: '' + }; +} + +export function runAdvisorHook({ event, payload = {}, pluginDir, dataDir, fsImpl = fs, env = process.env }) { + const safePluginDir = pluginDir || process.cwd(); + try { + const cfg = loadConfig(safePluginDir, fsImpl); + if (!cfg) return {}; + + const safeDataDir = dataDir || env.ZCODE_PLUGIN_DATA || env.CLAUDE_PLUGIN_DATA || path.join(safePluginDir, '.state'); + const sessionId = (typeof payload.session_id === 'string' && payload.session_id) || env.CLAUDE_SESSION_ID || 'default'; + const stateFile = path.join(safeDataDir, `${String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_')}.json`); + + let state = {}; + if (fsImpl.existsSync(stateFile)) { + try { + state = JSON.parse(fsImpl.readFileSync(stateFile, 'utf8')); + } catch { + state = {}; + } + } + if (sourceResetsSession(event, payload)) state = {}; + state = (state && typeof state === 'object') ? state : {}; + const fresh = freshState(); + for (const key of Object.keys(fresh)) { + if (state[key] === undefined) state[key] = fresh[key]; + } + state.modifiedFiles = state.modifiedFiles || {}; + state.verifiedFiles = state.verifiedFiles || {}; + state.circuitBreakerTripped = Boolean(state.circuitBreakerTripped); + + const persistState = () => { + if (!fsImpl.existsSync(safeDataDir)) fsImpl.mkdirSync(safeDataDir, { recursive: true }); + fsImpl.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8'); + }; + + const advisorLabel = cfg.roles?.advisor?.label || 'Frontier Advisor'; + const advisorAgent = cfg.roles?.advisor?.agentName || 'advisor-reviewer'; + const execLabel = cfg.roles?.executor?.label || 'Fast Executor'; + const execAgent = cfg.roles?.executor?.agentName || 'flash-executor'; + + const executorPatterns = (cfg.roles?.executor?.uiModelPatterns || ['flash']).map((p) => String(p).toLowerCase()); + const advisorPatterns = (cfg.roles?.advisor?.uiModelPatterns || ['glm-5.3']).map((p) => String(p).toLowerCase()); + const unknownModelRole = cfg.roles?.unknownModelRole === 'advisor' ? 'advisor' : 'executor'; + + const planBeforeComplexTask = cfg.gates?.planBeforeComplexTask !== false; + const maxFail = cfg.gates?.maxConsecutiveFailures !== undefined ? Number(cfg.gates.maxConsecutiveFailures) : 2; + const breakerEnabled = maxFail > 0; + const mandatoryFinalReview = cfg.gates?.mandatoryFinalReview !== false; + const maxStopContinuations = cfg.gates?.maxStopContinuations !== undefined ? Number(cfg.gates.maxStopContinuations) : 2; + + const breakerWarning = () => + `[TWO-STRIKE CIRCUIT BREAKER] A command failed repeatedly (reason: ${state.circuitBreakerReason || 'same_error'}). Stop retrying blindly. When running as the Executor, invoke subagent '${advisorAgent}' (${advisorLabel}) and pass the error output. When running as the Advisor, report Cause and Fix to the user.`; + const breakerDeny = () => + `[TWO-STRIKE CIRCUIT BREAKER] Execution blocked after repeated command failure. Invoke subagent '${advisorAgent}' (${advisorLabel}) with the error output before running further commands or edits.`; + const planDeny = () => + `[PRE-TASK PLAN GATE] Multi-file edit blocked. Invoke subagent '${advisorAgent}' (${advisorLabel}) to validate the architecture and the plan before modifying multiple files.`; + const finalReviewDeny = (files, status) => + `[MANDATORY FINAL REVIEW GATE] Cannot complete the task: modified files (${files.join(', ')}) lack an approved final review from '${advisorAgent}' matching their current file hashes (status: ${status || 'PENDING'}). Invoke subagent '${advisorAgent}' with the change summary before stopping.`; + + if (event === 'SessionStart') { + const rawModel = typeof payload.model === 'string' ? payload.model.trim().toLowerCase() : ''; + let role = state.role || ''; + if (rawModel) { + if (executorPatterns.some((p) => rawModel.includes(p))) role = 'executor'; + else if (advisorPatterns.some((p) => rawModel.includes(p))) role = 'advisor'; + else role = unknownModelRole; + } else if (!role) { + role = unknownModelRole; + } + state.role = role; + + const activeGates = []; + if (planBeforeComplexTask) activeGates.push('(1) planning and multi-file implementation'); + if (breakerEnabled) activeGates.push(`(2) after ${maxFail} consecutive identical failures`); + if (mandatoryFinalReview) activeGates.push(`(3) at the end of every coding task for the mandatory final review`); + + const defaultsDesc = `Defaults: simplicity=${cfg.defaults?.simplicityIntensity || 'full'}, actionFirst=${cfg.defaults?.actionFirstOutput !== false}.`; + let msg = ''; + if (role === 'executor') { + msg = activeGates.length === 0 + ? `[Advisor Mode: executor session (${rawModel || 'unknown model'})] You are running as the Executor (${execLabel}). All operational gates are disabled. ${defaultsDesc}` + : `[Advisor Mode: executor session (${rawModel || 'unknown model'})] You are running as the Executor (${execLabel}). Invoke subagent '${advisorAgent}' (${advisorLabel}) at the enabled gates: ${activeGates.join(', ')}. ${defaultsDesc}`; + } else { + msg = `[Advisor Mode: advisor session (${rawModel || 'unknown model'})] You are running as the Architect, Advisor, and Final Reviewer (${advisorLabel}). Delegate approved task implementation to subagent '${execAgent}' (${execLabel}) and review its diff and command exit codes before closing any task. ${defaultsDesc}`; + } + + let craft = ''; + try { + craft = fsImpl.readFileSync(path.join(safePluginDir, 'rules', 'craft.md'), 'utf8'); + } catch {} + + state.sessionStartedAt = new Date().toISOString(); + persistState(); + return { hookEventName: 'SessionStart', additionalContext: craft ? `${msg}\n\n${craft}` : msg }; + } + + if (event === 'UserPromptSubmit') { + state.stopContinuations = 0; + if (breakerEnabled && state.circuitBreakerTripped) { + persistState(); + return { hookEventName: 'UserPromptSubmit', additionalContext: breakerWarning() }; + } + persistState(); + return {}; + } + + const toolName = String(payload.tool_name || payload.toolName || payload.name || ''); + const toolInput = payload.tool_input ?? payload.toolInput ?? payload.args ?? {}; + + if (event === 'PreToolUse') { + if (breakerEnabled && state.circuitBreakerTripped && state.role === 'executor') { + if (COMMAND_TOOLS.has(toolName) || FILE_EDIT_TOOLS.has(toolName)) { + return { hookEventName: 'PreToolUse', continue: false, reason: breakerDeny() }; + } + } + + if (planBeforeComplexTask && state.role === 'executor' && FILE_EDIT_TOOLS.has(toolName)) { + const targetFile = extractTargetFile(toolInput); + const distinct = new Set(Object.keys(state.modifiedFiles || {})); + if (targetFile) distinct.add(targetFile); + if (distinct.size >= 2 && !state.planValidatedByAdvisor) { + return { hookEventName: 'PreToolUse', continue: false, reason: planDeny() }; + } + } + return {}; + } + + if (event === 'PostToolUseFailure') { + if (COMMAND_TOOLS.has(toolName)) { + registerFailure(payload, state, { maxFail, breakerEnabled }); + persistState(); + } + return {}; + } + + if (event === 'PostToolUse') { + if (COMMAND_TOOLS.has(toolName)) { + const failed = isCommandFailure(payload); + if (failed) { + registerFailure(payload, state, { maxFail, breakerEnabled }); + } else { + state.consecutiveFailures = 0; + state.sameErrorFailures = 0; + state.lastError = ''; + state.lastErrorSignature = ''; + state.circuitBreakerTripped = false; + state.circuitBreakerReason = ''; + } + persistState(); + return {}; + } + + if (FILE_EDIT_TOOLS.has(toolName)) { + const failed = payload?.error ? true : isCommandFailure(payload); + if (!failed) { + const targetFile = extractTargetFile(toolInput); + if (targetFile) { + const diskHash = hashFileOnDisk(targetFile, fsImpl); + state.modifiedFiles[targetFile] = { + hash: diskHash || null, + editedAt: new Date().toISOString() + }; + state.needsFinalReview = true; + state.reviewStatus = state.reviewStatus === 'APPROVED' ? 'STALE' : (state.reviewStatus || 'PENDING'); + } + } + persistState(); + return {}; + } + + if (AGENT_TOOLS.has(toolName)) { + const subagent = toolInput.subagent_type || toolInput.subagentType || toolInput.typeName || ''; + if (subagent === advisorAgent) { + state.planValidatedByAdvisor = true; + state.circuitBreakerTripped = false; + state.circuitBreakerReason = ''; + state.consecutiveFailures = 0; + state.sameErrorFailures = 0; + state.lastError = ''; + state.lastErrorSignature = ''; + + const outputText = stringifyResponse(payload.tool_response ?? payload.toolResponse ?? payload.result ?? ''); + if (outputText.includes('CHANGES_REQUIRED')) { + state.reviewStatus = 'CHANGES_REQUIRED'; + state.needsFinalReview = true; + state.lastReview = { reviewer: advisorAgent, verdict: 'CHANGES_REQUIRED', reviewedAt: new Date().toISOString() }; + } else if (outputText.includes('APPROVED')) { + state.verifiedFiles = {}; + for (const [filePath, meta] of Object.entries(state.modifiedFiles || {})) { + const currentHash = hashFileOnDisk(filePath, fsImpl) || meta.hash; + state.verifiedFiles[filePath] = currentHash; + if (currentHash) state.modifiedFiles[filePath].hash = currentHash; + } + if (Object.keys(state.modifiedFiles || {}).length > 0) { + state.reviewStatus = 'APPROVED'; + state.needsFinalReview = false; + state.lastReview = { reviewer: advisorAgent, verdict: 'APPROVED', verifiedFiles: { ...state.verifiedFiles }, reviewedAt: new Date().toISOString() }; + } + } + persistState(); + } + return {}; + } + return {}; + } + + if (event === 'Stop') { + if (!mandatoryFinalReview || state.role !== 'executor') return {}; + + const modifiedKeys = Object.keys(state.modifiedFiles || {}); + if (modifiedKeys.length === 0) return {}; + + let verificationFailed = state.needsFinalReview === true + || state.reviewStatus !== 'APPROVED' + || state.lastReview?.reviewer !== advisorAgent + || !state.verifiedFiles; + + if (!verificationFailed) { + for (const [filePath, meta] of Object.entries(state.modifiedFiles)) { + const verifiedHash = state.verifiedFiles[filePath]; + if (!verifiedHash || (meta.hash && verifiedHash !== meta.hash)) { + verificationFailed = true; + break; + } + const diskHash = hashFileOnDisk(filePath, fsImpl); + if (diskHash && diskHash !== verifiedHash) { + verificationFailed = true; + break; + } + } + } + + if (verificationFailed) { + if ((state.stopContinuations || 0) >= maxStopContinuations) { + state.reviewPendingAtStop = true; + persistState(); + return { + hookEventName: 'Stop', + additionalContext: `[MANDATORY FINAL REVIEW GATE] Stopping without an approved review from '${advisorAgent}'. Tell the user explicitly that the final review is still missing and which files changed.` + }; + } + state.stopContinuations = (state.stopContinuations || 0) + 1; + persistState(); + return { hookEventName: 'Stop', continue: true, reason: finalReviewDeny(modifiedKeys, state.reviewStatus) }; + } + + state.taskStatus = 'COMPLETED'; + state.reviewPendingAtStop = false; + state.modifiedFiles = {}; + state.verifiedFiles = {}; + state.needsFinalReview = false; + state.reviewStatus = ''; + state.lastReview = null; + state.planValidatedByAdvisor = false; + state.stopContinuations = 0; + persistState(); + return {}; + } + + return {}; + } catch (err) { + try { + const logDir = dataDir || path.join(safePluginDir, '.state'); + if (!fsImpl.existsSync(logDir)) fsImpl.mkdirSync(logDir, { recursive: true }); + fsImpl.appendFileSync(path.join(logDir, 'hook-errors.log'), `[${new Date().toISOString()}] [ERROR] event=${event}: ${err?.stack || err?.message || String(err)}\n`, 'utf8'); + } catch {} + return {}; + } +} + +function sourceResetsSession(event, payload) { + return event === 'SessionStart' && (payload?.source === 'clear' || payload?.source === 'startup'); +} + +function registerFailure(payload, state, { maxFail, breakerEnabled }) { + const sig = normalizeFailureSignature(payload); + state.consecutiveFailures = (Number(state.consecutiveFailures) || 0) + 1; + state.sameErrorFailures = (state.lastErrorSignature && state.lastErrorSignature === sig) ? ((Number(state.sameErrorFailures) || 0) + 1) : 1; + state.lastErrorSignature = sig; + state.lastError = failureErrorText(payload); + const shouldTrip = state.sameErrorFailures >= maxFail; + if (breakerEnabled && shouldTrip) { + state.circuitBreakerTripped = true; + state.circuitBreakerReason = 'same_error'; + } +} + +if (process.argv[1] && process.argv[1].endsWith('advisor-hook.mjs')) { + const event = process.argv[2] || ''; + const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + let rawInput = ''; + try { + if (process.stdin.isTTY !== true) { + rawInput = await new Promise((resolve) => { + let buf = ''; + const timer = setTimeout(() => resolve(buf), 2000); + process.stdin.setEncoding('utf8'); + process.stdin.on('data', (chunk) => { + buf += chunk; + }); + process.stdin.on('end', () => { + clearTimeout(timer); + resolve(buf); + }); + process.stdin.on('error', () => { + clearTimeout(timer); + resolve(buf); + }); + process.stdin.resume(); + }); + } + } catch { + rawInput = ''; + } + let payload = {}; + if (rawInput && rawInput.trim()) { + try { + payload = JSON.parse(rawInput); + } catch { + payload = {}; + } + } + const result = runAdvisorHook({ event, payload, pluginDir: pluginRoot, fsImpl: fs, env: process.env }); + if (result && Object.keys(result).length > 0) { + process.stdout.write(JSON.stringify(result)); + } + process.exit(0); +} diff --git a/external_plugins/advisor-mode-for-zcode/skills/advisor-mode/SKILL.md b/external_plugins/advisor-mode-for-zcode/skills/advisor-mode/SKILL.md new file mode 100644 index 0000000..d0b3852 --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/skills/advisor-mode/SKILL.md @@ -0,0 +1,46 @@ +--- +name: advisor-mode +description: Configure, inspect, or switch models and gates in Advisor Mode for Zcode (advisor-config.json and the agent model line). Use when the user asks to change the Advisor or Executor model, adjust failure thresholds, or check Advisor Mode status. +--- + +# Advisor Mode Configuration Skill + +Manage the Advisor Mode configuration stored in `advisor-config.json` inside the installed plugin directory, and the model line of the advisor agent. + +## Roles and how models are chosen + +- The Executor agent (`agents/flash-executor.md`) uses `model: inherit`, so it always runs on the model the user selected for the session. Switching the session model is enough to change the executor; no edit is needed. +- The Advisor agent (`agents/advisor-reviewer.md`) pins a frontier model on its `model:` frontmatter line because ZCode resolves subagent models from the agent definition. The line must carry an explicit reasoning level with the `$` suffix (for example `model: account:/GLM-5.3$max`): ZCode rejects a pinned reasoning-capable model without one. When a new frontier model is released, update that single line and restart the session. This is the only line that pins a concrete model. +- The role of the current session is detected at SessionStart from the session model id matched against `roles.executor.uiModelPatterns` and `roles.advisor.uiModelPatterns` (case-insensitive substrings, executor patterns checked first). An unrecognized model falls back to `roles.unknownModelRole`, which defaults to `executor` so unknown models fail closed under the gates. + +## Configuration structure + +`advisor-config.json` controls: + +- `enabled` (boolean): toggles the lifecycle hook injections without uninstalling the plugin. +- `roles.unknownModelRole`: default role (`executor` or `advisor`) for unrecognized session models. +- `roles.advisor`: `label`, `agentName`, `uiModelPatterns`. +- `roles.executor`: `label`, `agentName`, `uiModelPatterns`. +- `gates.planBeforeComplexTask`: pre-task architecture validation before multi-file edits (default `true`). +- `gates.maxConsecutiveFailures`: identical failures before the Two-Strike Circuit Breaker trips (default `2`, `0` disables). +- `gates.mandatoryFinalReview`: approved final review with file hash verification before task completion (default `true`). +- `gates.maxStopContinuations`: how many times the Stop gate may force the session back to work before it warns instead (default `2`). +- `defaults.simplicityIntensity`, `defaults.actionFirstOutput`: baseline rigor flags reported at session start. + +## Lifecycle hooks enforced + +The plugin registers hooks on six ZCode events: + +1. `SessionStart`: detects the role from the session model and injects the gate directives plus the craft standards. +2. `UserPromptSubmit`: injects a warning while the circuit breaker is active. +3. `PreToolUse` (Bash, Write, Edit): blocks commands and edits while the breaker is tripped; blocks multi-file edits without an approved plan. +4. `PostToolUse` (Bash, Write, Edit, Agent): tracks failure signatures, records modified file hashes, and captures advisor verdicts from completed advisor subagent runs. +5. `PostToolUseFailure` (Bash): counts command failures that report through the failure event. +6. `Stop`: enforces the mandatory final review; forces the session back to work (output `continue: true`) until the advisor approves the current file hashes, then resets the task state. + +## Updating configuration and verifying + +1. Edit `advisor-config.json` in the installed plugin directory to update labels, agent names, patterns, or gate switches. +2. To change the advisor model, edit the `model:` line in `agents/advisor-reviewer.md`. +3. Run the test suite from the plugin source directory: `node --test tests/advisor-hook.test.mjs`. +4. Dry run a session start: `node ./scripts/advisor-hook.mjs SessionStart` with a stdin payload containing the session model. diff --git a/external_plugins/advisor-mode-for-zcode/tests/advisor-hook.test.mjs b/external_plugins/advisor-mode-for-zcode/tests/advisor-hook.test.mjs new file mode 100644 index 0000000..de1667f --- /dev/null +++ b/external_plugins/advisor-mode-for-zcode/tests/advisor-hook.test.mjs @@ -0,0 +1,264 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawn } from 'node:child_process'; + +import { runAdvisorHook } from '../scripts/advisor-hook.mjs'; + +const pluginRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const defaultConfig = JSON.parse(fs.readFileSync(path.join(pluginRoot, 'advisor-config.json'), 'utf8')); + +function runCli(scriptPath, args, payloadObj, dataDir) { + return new Promise((resolve, reject) => { + const child = spawn('node', [scriptPath, ...args], { env: { ...process.env, ZCODE_PLUGIN_DATA: dataDir } }); + let stdout = ''; + child.stdout.on('data', (chunk) => { + stdout += chunk; + }); + child.stderr.on('data', (chunk) => { + stdout += chunk; + }); + child.on('error', reject); + child.on('exit', (code) => resolve({ stdout, code })); + child.stdin.write(JSON.stringify(payloadObj)); + child.stdin.end(); + }); +} + +function makeTemp() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'advisor-zcode-test-')); +} + +function makePluginDir(config) { + const dir = makeTemp(); + fs.writeFileSync(path.join(dir, 'advisor-config.json'), JSON.stringify(config, null, 2), 'utf8'); + fs.mkdirSync(path.join(dir, 'rules'), { recursive: true }); + fs.writeFileSync(path.join(dir, 'rules', 'craft.md'), '# Craft Standards\nTest craft rules content.', 'utf8'); + return dir; +} + +function makeEnv(pluginDir) { + return { pluginDir, dataDir: makeTemp(), fsImpl: fs, env: {} }; +} + +function writeSessionFile(dataDir, name, content) { + const file = path.join(dataDir, name); + fs.writeFileSync(file, content, 'utf8'); + return file; +} + +test('1. Role recognition: flash, frontier, unknown, and missing session models', () => { + const pluginDir = makePluginDir(defaultConfig); + const sessionId = 'role-test'; + + const flash = runAdvisorHook({ event: 'SessionStart', payload: { session_id: sessionId, source: 'startup', model: 'account:zai-start-plan/GLM-5.3-Flash' }, ...makeEnv(pluginDir) }); + assert.equal(flash.hookEventName, 'SessionStart'); + assert.ok(flash.additionalContext.includes('executor session'), 'flash model must map to executor'); + assert.ok(flash.additionalContext.includes('Test craft rules content'), 'craft rules must be injected'); + + const frontier = runAdvisorHook({ event: 'SessionStart', payload: { session_id: sessionId, source: 'startup', model: 'account:zai-individual-coding-plan/GLM-5.3' }, ...makeEnv(pluginDir) }); + assert.ok(frontier.additionalContext.includes('advisor session'), 'frontier model must map to advisor'); + + const unknown = runAdvisorHook({ event: 'SessionStart', payload: { session_id: sessionId, source: 'startup', model: 'account:other-plan/mystery' }, ...makeEnv(pluginDir) }); + assert.ok(unknown.additionalContext.includes('executor session'), 'unknown model must fail closed to executor'); + + const noModel = runAdvisorHook({ event: 'SessionStart', payload: { session_id: sessionId, source: 'startup' }, ...makeEnv(pluginDir) }); + assert.ok(noModel.additionalContext.includes('executor session'), 'missing model must fail closed to executor'); +}); + +test('2. Gates governance: disabled gates relax injection and enforcement', () => { + const relaxed = JSON.parse(JSON.stringify(defaultConfig)); + relaxed.gates.planBeforeComplexTask = false; + relaxed.gates.maxConsecutiveFailures = 0; + relaxed.gates.mandatoryFinalReview = false; + const pluginDir = makePluginDir(relaxed); + const sessionId = 'gates-off'; + const ctx = makeEnv(pluginDir); + + const start = runAdvisorHook({ event: 'SessionStart', payload: { session_id: sessionId, source: 'startup', model: 'account:zai-start-plan/GLM-5.3-Flash' }, ...ctx }); + assert.ok(start.additionalContext.includes('All operational gates are disabled'), 'disabled gates must be announced'); + + const fileA = writeSessionFile(ctx.dataDir, 'gates-a.txt', 'a'); + const fileB = writeSessionFile(ctx.dataDir, 'gates-b.txt', 'b'); + runAdvisorHook({ event: 'PostToolUse', payload: { session_id: sessionId, tool_name: 'Write', tool_input: { file_path: fileA, content: 'a' } }, ...ctx }); + const secondEdit = runAdvisorHook({ event: 'PreToolUse', payload: { session_id: sessionId, tool_name: 'Edit', tool_input: { file_path: fileB, old_string: 'b', new_string: 'c' } }, ...ctx }); + assert.deepEqual(secondEdit, {}, 'plan gate must stay open when disabled'); + + runAdvisorHook({ event: 'PostToolUse', payload: { session_id: sessionId, tool_name: 'Write', tool_input: { file_path: fileB, content: 'c' } }, ...ctx }); + const stop = runAdvisorHook({ event: 'Stop', payload: { session_id: sessionId }, ...ctx }); + assert.deepEqual(stop, {}, 'stop gate must stay open when disabled'); +}); + +test('3. Error detection and failure signature tracking', () => { + const pluginDir = makePluginDir(defaultConfig); + const sessionId = 'failures'; + const ctx = makeEnv(pluginDir); + + const failPayload = (stderr, command = 'npm test') => ({ + session_id: sessionId, + tool_name: 'Bash', + tool_input: { command }, + tool_response: { exitCode: 1, stderr } + }); + + runAdvisorHook({ event: 'PostToolUseFailure', payload: failPayload('Error ECONNREFUSED 127.0.0.1:5432 at 2026-10-23T10:00:00Z'), ...ctx }); + const state1 = JSON.parse(fs.readFileSync(path.join(ctx.dataDir, `${sessionId}.json`), 'utf8')); + assert.equal(state1.sameErrorFailures, 1); + + runAdvisorHook({ event: 'PostToolUse', payload: failPayload('Error ECONNREFUSED 127.0.0.1:5433 at 2026-10-23T11:00:00Z'), ...ctx }); + const state2 = JSON.parse(fs.readFileSync(path.join(ctx.dataDir, `${sessionId}.json`), 'utf8')); + assert.equal(state2.circuitBreakerTripped, true, 'normalized identical signatures must trip the breaker on the second strike'); + assert.equal(state2.sameErrorFailures, 2); + + runAdvisorHook({ event: 'PostToolUse', payload: failPayload('a completely different failure'), ...ctx }); + const state3 = JSON.parse(fs.readFileSync(path.join(ctx.dataDir, `${sessionId}.json`), 'utf8')); + assert.equal(state3.sameErrorFailures, 1, 'a different signature must restart the same-error count'); + + runAdvisorHook({ event: 'PostToolUse', payload: { session_id: sessionId, tool_name: 'Bash', tool_input: { command: 'npm test' }, tool_response: { exitCode: 0 } }, ...ctx }); + const state4 = JSON.parse(fs.readFileSync(path.join(ctx.dataDir, `${sessionId}.json`), 'utf8')); + assert.equal(state4.circuitBreakerTripped, false, 'a successful command must reset the breaker'); +}); + +test('4. Circuit breaker persistence and advisor consultation release', () => { + const pluginDir = makePluginDir(defaultConfig); + const sessionId = 'breaker'; + const ctx = makeEnv(pluginDir); + + runAdvisorHook({ event: 'SessionStart', payload: { session_id: sessionId, source: 'startup', model: 'account:zai-start-plan/GLM-5.3-Flash' }, ...ctx }); + for (let i = 0; i < 2; i++) { + runAdvisorHook({ event: 'PostToolUseFailure', payload: { session_id: sessionId, tool_name: 'Bash', tool_input: { command: 'git push' }, tool_response: { exitCode: 128, stderr: 'rejected' } }, ...ctx }); + } + + const prompt = runAdvisorHook({ event: 'UserPromptSubmit', payload: { session_id: sessionId }, ...ctx }); + assert.equal(prompt.hookEventName, 'UserPromptSubmit'); + assert.ok(prompt.additionalContext.includes('TWO-STRIKE CIRCUIT BREAKER'), 'an active breaker must warn on new prompts'); + + const blocked = runAdvisorHook({ event: 'PreToolUse', payload: { session_id: sessionId, tool_name: 'Bash', tool_input: { command: 'git push' } }, ...ctx }); + assert.equal(blocked.continue, false, 'an active breaker must deny commands'); + assert.ok(blocked.reason.includes('advisor-reviewer')); + + const consultation = runAdvisorHook({ + event: 'PostToolUse', + payload: { + session_id: sessionId, + tool_name: 'Agent', + tool_input: { subagent_type: 'advisor-reviewer', prompt: 'diagnose the push failure' }, + tool_response: { content: [{ type: 'text', text: 'Cause: remote hook rejected the branch.\nFix: pull and rebase first.' }] } + }, + ...ctx + }); + assert.deepEqual(consultation, {}); + + const allowed = runAdvisorHook({ event: 'PreToolUse', payload: { session_id: sessionId, tool_name: 'Bash', tool_input: { command: 'git push' } }, ...ctx }); + assert.deepEqual(allowed, {}, 'an advisor consultation must release the breaker'); +}); + +test('5. Pre-task plan gate on multi-file edits', () => { + const pluginDir = makePluginDir(defaultConfig); + const sessionId = 'plan-gate'; + const ctx = makeEnv(pluginDir); + + runAdvisorHook({ event: 'SessionStart', payload: { session_id: sessionId, source: 'startup', model: 'account:zai-start-plan/GLM-5.3-Flash' }, ...ctx }); + + const fileA = writeSessionFile(ctx.dataDir, 'a.txt', 'a'); + const fileB = writeSessionFile(ctx.dataDir, 'b.txt', 'b'); + const firstEdit = runAdvisorHook({ event: 'PreToolUse', payload: { session_id: sessionId, tool_name: 'Edit', tool_input: { file_path: fileA } }, ...ctx }); + assert.deepEqual(firstEdit, {}, 'a single-file edit must pass'); + + runAdvisorHook({ event: 'PostToolUse', payload: { session_id: sessionId, tool_name: 'Edit', tool_input: { file_path: fileA } }, ...ctx }); + const secondEdit = runAdvisorHook({ event: 'PreToolUse', payload: { session_id: sessionId, tool_name: 'Edit', tool_input: { file_path: fileB } }, ...ctx }); + assert.equal(secondEdit.continue, false, 'a second distinct file must be denied before plan validation'); + assert.ok(secondEdit.reason.includes('PRE-TASK PLAN GATE')); + + runAdvisorHook({ + event: 'PostToolUse', + payload: { + session_id: sessionId, + tool_name: 'Agent', + tool_input: { subagent_type: 'advisor-reviewer', prompt: 'validate the plan' }, + tool_response: { content: [{ type: 'text', text: 'Plan validated. Proceed.' }] } + }, + ...ctx + }); + const thirdEdit = runAdvisorHook({ event: 'PreToolUse', payload: { session_id: sessionId, tool_name: 'Edit', tool_input: { file_path: fileB } }, ...ctx }); + assert.deepEqual(thirdEdit, {}, 'an advisor-validated plan must open the gate'); +}); + +test('6. Final review gate: verdict capture, hash verification, and stop continuation cap', () => { + const pluginDir = makePluginDir(defaultConfig); + const sessionId = 'final-review'; + const ctx = makeEnv(pluginDir); + + runAdvisorHook({ event: 'SessionStart', payload: { session_id: sessionId, source: 'startup', model: 'account:zai-start-plan/GLM-5.3-Flash' }, ...ctx }); + + const fileA = writeSessionFile(ctx.dataDir, 'work.txt', 'version 1'); + runAdvisorHook({ event: 'PostToolUse', payload: { session_id: sessionId, tool_name: 'Write', tool_input: { file_path: fileA, content: 'version 1' } }, ...ctx }); + + runAdvisorHook({ + event: 'PostToolUse', + payload: { + session_id: sessionId, + tool_name: 'Agent', + tool_input: { subagent_type: 'advisor-reviewer', prompt: 'final review' }, + tool_response: { content: [{ type: 'text', text: 'Scope clean.\nnet: -0 lines possible.\nLean already. Ship.\nVERDICT: APPROVED' }] } + }, + ...ctx + }); + + const stopApproved = runAdvisorHook({ event: 'Stop', payload: { session_id: sessionId }, ...ctx }); + assert.deepEqual(stopApproved, {}, 'an approved review matching disk hashes must let the task stop'); + + const stateAfter = JSON.parse(fs.readFileSync(path.join(ctx.dataDir, `${sessionId}.json`), 'utf8')); + assert.equal(stateAfter.taskStatus, 'COMPLETED'); + assert.equal(Object.keys(stateAfter.modifiedFiles).length, 0, 'task state must reset after a completed stop'); + + runAdvisorHook({ event: 'PostToolUse', payload: { session_id: sessionId, tool_name: 'Write', tool_input: { file_path: fileA, content: 'version 2' } }, ...ctx }); + fs.writeFileSync(fileA, 'version 2', 'utf8'); + + const stopStale = runAdvisorHook({ event: 'Stop', payload: { session_id: sessionId }, ...ctx }); + assert.equal(stopStale.continue, true, 'a stale review must force continuation'); + assert.ok(stopStale.reason.includes('MANDATORY FINAL REVIEW GATE')); + + const stopSecond = runAdvisorHook({ event: 'Stop', payload: { session_id: sessionId, stop_hook_active: true }, ...ctx }); + assert.equal(stopSecond.continue, true, 'the second continuation must still force work'); + assert.equal(stopSecond.reason.includes('MANDATORY FINAL REVIEW GATE'), true); + + const stopCapped = runAdvisorHook({ event: 'Stop', payload: { session_id: sessionId, stop_hook_active: true }, ...ctx }); + assert.equal(stopCapped.continue, undefined, 'past the cap the gate must warn instead of forcing'); + assert.ok(stopCapped.additionalContext.includes('final review is still missing')); + + fs.writeFileSync(fileA, 'tampered without review', 'utf8'); + runAdvisorHook({ + event: 'PostToolUse', + payload: { + session_id: sessionId, + tool_name: 'Agent', + tool_input: { subagent_type: 'advisor-reviewer', prompt: 'final review' }, + tool_response: { content: [{ type: 'text', text: 'VERDICT: CHANGES_REQUIRED' }] } + }, + ...ctx + }); + const stopChanges = runAdvisorHook({ event: 'Stop', payload: { session_id: sessionId }, ...ctx }); + assert.ok( + (stopChanges.reason && stopChanges.reason.includes('MANDATORY FINAL REVIEW GATE')) || stopChanges.additionalContext, + 'a changes-required verdict must keep the gate active' + ); +}); + +test('7. CLI execution: standalone stdin and stdout contract', async () => { + const dataDir = makeTemp(); + const script = path.join(pluginRoot, 'scripts', 'advisor-hook.mjs'); + const first = await runCli(script, ['SessionStart'], { session_id: 'cli-test', source: 'startup', model: 'account:zai-start-plan/GLM-5.3-Flash' }, dataDir); + assert.equal(first.code, 0); + const parsed = JSON.parse(first.stdout); + assert.equal(parsed.hookEventName, 'SessionStart'); + assert.ok(parsed.additionalContext.includes('Advisor Mode')); + assert.ok(parsed.additionalContext.includes('executor session'), 'the CLI must read the session model from stdin'); + + const quiet = await runCli(script, ['UserPromptSubmit'], { session_id: 'cli-test' }, dataDir); + assert.equal(quiet.stdout, '', 'an action-free event must print nothing'); + assert.ok(fs.existsSync(path.join(dataDir, 'cli-test.json')), 'state must persist under the plugin data directory'); +}); diff --git a/marketplace.json b/marketplace.json index 884bfc8..b58cc63 100644 --- a/marketplace.json +++ b/marketplace.json @@ -11,6 +11,22 @@ }, "unlistedPlugins": [ { "name": "openpencil", "source": "./external_plugins/openpencil" } +, + { + "name": "advisor-mode-for-zcode", + "displayName": "Advisor Mode for Zcode", + "displayName_i18n": { "en": "Advisor Mode for Zcode", "zh-CN": "Advisor Mode for Zcode" }, + "source": "./external_plugins/advisor-mode-for-zcode", + "description": "Dual-tier advisor and executor workflow for ZCode with hard lifecycle gates, minimalist engineering, clean prose, and action-first ergonomics.", + "description_i18n": { + "en": "Dual-tier advisor and executor workflow for ZCode with hard lifecycle gates, minimalist engineering, clean prose, and action-first ergonomics.", + "zh-CN": "面向 ZCode 的双层工程工作流:前沿顾问负责审阅,跟随会话模型的快速执行者负责实施,并以硬性生命周期门禁、极简工程、洁净文风与行动优先工效加以约束。" + }, + "version": "0.1.0", + "author": { "name": "Gianluca Iannotta", "url": "https://github.com/jannotix" }, + "category": "productivity", + "keywords": ["advisor-mode", "subagents", "code-review", "yagni", "minimalist-engineering"] + } ], "plugins": [ {