@xgent-ai/skills 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +94 -4
  2. package/bin/xgent-skills.js +221 -17
  3. package/docs/claude-context-goal-guard.md +124 -0
  4. package/docs/codex-context-goal-guard.md +109 -0
  5. package/hooks/claude/context-goal-guard.js +139 -0
  6. package/hooks/claude/settings.example.json +26 -0
  7. package/hooks/codex/context-goal-guard.py +189 -0
  8. package/hooks/codex/hooks.example.json +26 -0
  9. package/package.json +7 -1
  10. package/skills/agi-mode/SKILL.md +27 -2
  11. package/skills/agi-mode/assets/directory-index.md +18 -0
  12. package/skills/agi-mode/assets/sdlc/event.md +33 -0
  13. package/skills/agi-mode/assets/sdlc/index.md +17 -0
  14. package/skills/agi-mode/assets/sdlc/object-status.md +25 -0
  15. package/skills/agi-mode/assets/sdlc/protocol.md +98 -0
  16. package/skills/agi-mode/assets/sdlc/state-model.md +78 -0
  17. package/skills/agi-mode/assets/sdlc/type-index.md +10 -0
  18. package/skills/agi-mode/evals/scenarios.md +45 -0
  19. package/skills/agi-mode/playbooks/program.md +1 -1
  20. package/skills/agi-mode/playbooks/reflection.md +8 -0
  21. package/skills/agi-mode/playbooks/sdlc.md +99 -0
  22. package/skills/agi-mode/playbooks/what-next.md +79 -0
  23. package/skills/agi-mode/references/continuity.md +3 -1
  24. package/skills/agi-mode/references/workspace-layout.md +76 -0
  25. package/skills/apply-code-review/SKILL.md +17 -1
  26. package/skills/apply-code-review/references/record-contract.md +14 -1
  27. package/skills/apply-doc-review/SKILL.md +30 -6
  28. package/skills/apply-doc-review/references/disposition-contract.md +15 -2
  29. package/skills/archify/SKILL.md +1 -1
  30. package/skills/architect/SKILL.md +1 -1
  31. package/skills/arena/SKILL.md +1 -1
  32. package/skills/automate-me/SKILL.md +1 -1
  33. package/skills/autoresearch/SKILL.md +1 -1
  34. package/skills/crd/SKILL.md +106 -0
  35. package/skills/crd/assets/cr-template.md +66 -0
  36. package/skills/debugging/SKILL.md +1 -1
  37. package/skills/dev-plan/SKILL.md +16 -7
  38. package/skills/dev-plan/references/plan-skeleton.md +8 -6
  39. package/skills/dev-plan/scripts/check_progress.sh +2 -2
  40. package/skills/documentation-writer/SKILL.md +1 -1
  41. package/skills/elintp/SKILL.md +1 -1
  42. package/skills/experience-capture/SKILL.md +64 -0
  43. package/skills/experience-capture/agents/openai.yaml +5 -0
  44. package/skills/figure-it-out/SKILL.md +1 -1
  45. package/skills/gdpr-compliant/SKILL.md +1 -1
  46. package/skills/how/SKILL.md +1 -1
  47. package/skills/humanize-writing/SKILL.md +1 -1
  48. package/skills/kpi-dashboard-design/SKILL.md +1 -1
  49. package/skills/landing-page-conversion-audit/SKILL.md +1 -1
  50. package/skills/meeting-minutes/SKILL.md +1 -1
  51. package/skills/pardon/SKILL.md +1 -1
  52. package/skills/portal-app-exchange/SKILL.md +1 -1
  53. package/skills/portal-dev-setup/SKILL.md +1 -1
  54. package/skills/portal-external-app/SKILL.md +1 -1
  55. package/skills/portal-logging/SKILL.md +1 -1
  56. package/skills/portal-micro-app/SKILL.md +1 -1
  57. package/skills/prd/SKILL.md +8 -1
  58. package/skills/react/SKILL.md +1 -11
  59. package/skills/reflect/SKILL.md +1 -1
  60. package/skills/review-code/SKILL.md +25 -4
  61. package/skills/review-code/references/report-contract.md +17 -1
  62. package/skills/review-dev-plan/SKILL.md +22 -2
  63. package/skills/review-dev-plan/references/report-contract.md +30 -0
  64. package/skills/review-prd/SKILL.md +22 -2
  65. package/skills/review-prd/references/report-contract.md +30 -0
  66. package/skills/review-prd-dev-gaps/SKILL.md +24 -4
  67. package/skills/review-prd-dev-gaps/references/report-contract.md +14 -0
  68. package/skills/rust/SKILL.md +1 -10
  69. package/skills/security-audit/SKILL.md +1 -1
  70. package/skills/sql-optimization/SKILL.md +1 -1
  71. package/skills/story-points/SKILL.md +2 -1
  72. package/skills/summarize/SKILL.md +1 -1
  73. package/skills/swarm/SKILL.md +1 -1
  74. package/skills/tdd/SKILL.md +1 -1
  75. package/skills/teach/SKILL.md +1 -1
  76. package/skills/test-plan/SKILL.md +2 -1
  77. package/skills/why/SKILL.md +1 -1
  78. package/skills/working-with-mbx/SKILL.md +1 -1
  79. package/skills/working-with-mise/SKILL.md +1 -1
  80. package/skills/xgent-app-release/SKILL.md +1 -1
  81. package/skills/xgent-image-push/SKILL.md +1 -1
  82. package/skills/xgent-init/SKILL.md +1 -1
@@ -0,0 +1,109 @@
1
+ # Codex goal 上下文收尾提醒
2
+
3
+ 当本地估算上下文使用率**严格超过阈值**(默认 65%,可配置)且当前根线程的 goal 为 `active` 时,本 hook 提醒 agent 收尾:更新已有任务记录、交接未完成项、按用户明确的暂停策略调用原生 goal 工具,再提醒用户自行新开对话。恰好等于阈值不触发。hook 只读取本地数据,不写进度、修改 goal 或创建对话。
4
+
5
+ ## 兼容范围与限制
6
+
7
+ 首期格式限定为 Codex CLI / App Server **0.160.0**。已验证环境为 macOS arm64、Python 3.13.7;运行服务标识及完整验收见 [M1 记录](plan/codex-context-goal-guard.records/M1.md)与 [M3 记录](plan/codex-context-goal-guard.records/M3.md)。Python 至少需要 3.9。后续 Codex 版本不自动视为兼容,未知格式会报告检测不可用。
8
+
9
+ 估算值来自当前会话最新完整 `token_count` 的 `last_token_usage.total_tokens / model_context_window`,可能滞后于 UI。累计 tokens、cached tokens 和 reasoning tokens 不重复加入。压缩或模型/窗口改变后,没有新统计时不沿用旧值。子 agent 跳过;fork 只读取自己的 goal。读取位置由真实 hook 输入和 `CODEX_HOME` 决定,未设置时使用 `~/.codex`。
10
+
11
+ 检查发生在同步 `PreToolUse` 和 `Stop` 边界;已经调度的调用可能完成,部分工具路径没有 tool hook。提醒依赖 agent 执行,不能强制暂停。首次 Stop 可以请求补做收尾,第二次仍未暂停时只告警;此告警不能阻止 goal 自动续跑。上述事件、信任及输出语义见 [官方 Hooks 文档](https://learn.chatgpt.com/docs/hooks)。
12
+
13
+ ## 获取与部署
14
+
15
+ ### 使用 install
16
+
17
+ 功能已接入 `install` 源码和 npm 打包清单,尚未发布到 npm。当前从本仓库根目录运行:
18
+
19
+ ```sh
20
+ # 默认不安装 guard;需要时显式选择安装
21
+ node bin/xgent-skills.js install /absolute/path/to/project --context-goal-guard
22
+ ```
23
+
24
+ 发布包含此功能的新版本后,可用 `npx @xgent-ai/skills install --context-goal-guard`。交互和非交互安装均默认跳过 guard,不再询问。仅在显式传入 `--context-goal-guard` 或 `--context-goal-guard-threshold=N` 时安装;`--no-context-goal-guard` 显式跳过并保留已有 guard,不能与安装参数同时使用。此选择独立于 `--providers`、`--no-impeccable` 和 `--xgent-init`。
25
+
26
+ 请安装到目标 Git 仓库根目录。安装器检查 `python3` 为 3.9+ 且包含 SQLite 标准库,然后复制脚本到目标项目 `.codex/hooks/context-goal-guard.py`,将两个同步 handler 合并到 `.codex/hooks.json`,使用 `$(git rev-parse --show-toplevel)` 在运行时定位脚本,超时设为 3 秒,配置不包含本机项目绝对路径。其他事件、handler、matcher 和顶层字段保留;重复安装更新 guard,不重复添加,也会替换旧版 guard 的绝对路径命令。非法 JSON 默认报错,`--force` 可先备份为 `.bak` 再覆盖;合法 JSON 的未知结构需手动修复。
27
+
28
+ Python 缺失或版本不足时,按安装器提示先安装 Python 并运行 `python3 --version` 核对,再重试;也可用 `--no-context-goal-guard` 完成其他安装。Codex 运行环境必须能解析 `git` 和 `python3`;后者无法解析时,按下方手动配置说明改为解释器绝对路径。
29
+
30
+ 安装结束会打印下方「信任与生效确认」的操作步骤及可复制的当前 goal 暂停策略。**脚本已部署不等于已生效**:用户仍需在 Codex 内信任项目、审阅并信任启用两个 handler、明确当前 goal 的暂停策略,并用测试 goal 验证真实暂停。安装器不代替用户完成这些步骤。
31
+
32
+ ### 阈值配置
33
+
34
+ 阈值写在 handler 命令末尾:`--threshold N`,N 为 1–99 的整数百分比;不带参数时为 65。与 Claude Code 版(默认 70)各自独立。
35
+
36
+ ```sh
37
+ node bin/xgent-skills.js install /absolute/path/to/project --context-goal-guard-threshold=60
38
+ ```
39
+
40
+ `--context-goal-guard-threshold=N` 同时表示安装,不再询问;与 `--no-context-goal-guard` 不能同时使用。安装器总是把阈值写进两个 handler 的 command,重装时不给这个 flag 就沿用已装的值;安装输出里可复制的暂停策略也使用实际阈值。改动 command 后需要在 `/hooks` 中重新审阅、信任。参数无效时 guard 报告检测不可用,不猜测数值。
41
+
42
+ ### 手动部署
43
+
44
+ 也可单独复制仓库文件。首期手动部署的精确本地验证 revision 记在 [M3 记录](plan/codex-context-goal-guard.records/M3.md):在可访问该本地仓库的机器 clone 并 checkout 该 revision(该 revision 不含后续 install 集成)。远程发布后,才可从 `https://github.com/XGENT-ai/skills.git` checkout 对应已发布 revision;不要将尚未发布的 SHA 当作远程可获取版本。
45
+
46
+ ```sh
47
+ git clone /absolute/path/to/skills /tmp/skills-guard-source
48
+ git -C /tmp/skills-guard-source checkout <M3记录中的本地验证revision>
49
+ ```
50
+
51
+ 先核对 `codex --version`、`python3 --version` 和 `git --version`。以下复制命令中的目标目录替换为实际 Git 仓库根目录;可在目标项目运行 `git rev-parse --show-toplevel` 确认。
52
+
53
+ ```sh
54
+ mkdir -p "/absolute/path/to/My Project/.codex/hooks"
55
+ cp "/tmp/skills-guard-source/hooks/codex/context-goal-guard.py" "/absolute/path/to/My Project/.codex/hooks/context-goal-guard.py"
56
+ ```
57
+
58
+ 打开目标项目 `.codex/hooks.json`,将 [配置示例](../hooks/codex/hooks.example.json)中的两组 handler **追加**到已有 `PreToolUse` 和 `Stop` 数组;没有文件或事件时才新建对应对象。保留其他事件、handler、matcher 和顶层字段。不要用整个示例覆盖已有 manifest。
59
+
60
+ 两个 command 保留以下写法及 JSON 内转义的双引号,无需填入本机目录;运行时按 Git 根目录解析,可从仓库子目录启动,也可随仓库迁移:[官方 Hooks 路径约定](https://learn.chatgpt.com/docs/hooks)。
61
+
62
+ ```json
63
+ {"type": "command", "command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/context-goal-guard.py\" --threshold 65", "timeout": 3}
64
+ ```
65
+
66
+ 若 Codex 环境无法解析 `python3`,将其换为已核对的 Python 3 可执行文件绝对路径,也保留必要引号。Git 根目录的命令替换结果置于双引号内,含空格或 shell 元字符的项目路径仍作为单个参数传入。同步超时为 3 秒。非 Git 目录不适用此写法,需手动配置脚本绝对路径。
67
+
68
+ ## 信任与生效确认
69
+
70
+ 1. 从目标项目启动 Codex,审阅并信任项目 `.codex/` 层。
71
+ 2. 打开 `/hooks`,找到来源为本项目 `.codex/hooks.json` 的两个 guard handler;审阅命令、路径、事件与超时,分别信任并启用。
72
+ 3. 再次确认两个 handler 的来源、启用和信任状态;确认其他 hooks 仍在列表中。未信任时会被跳过,安静无告警不能证明已生效。
73
+ 4. 修改 hook 定义或 command 路径后,重新在 `/hooks` 审阅信任。信任绑定定义 hash,不伪造 hash。替换脚本内容也需自行重新审阅;不要假定定义 hash 会检测脚本内容变化。
74
+ 5. 在隔离项目、测试 goal 中验证提示和真实暂停后,再用于日常任务。可从仓库子目录启动,command 仍从 Git 根目录定位脚本。
75
+
76
+ 安装、信任和阅读说明都不构成暂停授权。在**当前 goal** 的用户指令里明确,例如:
77
+
78
+ > 对当前 goal,如果本地估算上下文使用率严格超过 65%(按实际阈值替换),请先安全收尾,按已有任务约定保存进度和交接说明,再使用原生 goal 工具将其设为 paused 并核对,无需二次确认。未完成工作不要标为 complete。请提醒我自己新开对话继续。
79
+
80
+ 后续 `/goal resume` 撤销先前暂停请求,需重新明确策略。普通命令测试可使用 `features.code_mode.enabled=true` 和 `features.code_mode.direct_only_tool_namespaces=["functions"]`;code mode 使用默认路径。M1 验证了两者;仅关闭 code mode 不保证普通命令回退。配置字段见 [官方配置参考](https://learn.chatgpt.com/docs/config-file/config-reference)。
81
+
82
+ ## 收尾与人工接续
83
+
84
+ 成功回复应说明真实 `paused` 状态、已有记录位置、剩余任务和下一步。无既有记录时,agent 在回复中给出交接摘要(完成/未完成、验证/未验证、下一步、仍运行的进程),不强制创建新文件。进度写入失败时,也先在回复保留最小摘要并明确失败。
85
+
86
+ 在同一项目目录自行新开对话,提供已有计划路径,或粘贴交接摘要并说明继续。新对话没有继承的 goal;需要用户手动设置 goal、重新明确按阈值暂停的策略后,guard 才对它生效。旧 goal 保持 paused;可保留,或回旧对话主动 `/goal clear`。恢复旧 goal 不清空上下文,不作为腾出上下文的办法。线程生命周期命令见 [官方 Goals 文档](https://developers.openai.com/cookbook/examples/codex/using_goals_in_codex)。
87
+
88
+ 如果 agent 请求确认、拒绝暂停或暂停失败,仍算尚未暂停;在原对话执行 `/goal pause` 后再接续。不要依据“已提醒”判断暂停成功。
89
+
90
+ ## 排查与停用
91
+
92
+ | 表现 | 检查与处理 |
93
+ | --- | --- |
94
+ | `/hooks` 没有 guard 或显示待审/禁用 | 核对项目层信任、来源、manifest JSON,以及 `git rev-parse --show-toplevel` 下的脚本路径;重新审阅信任并启用 |
95
+ | “检测不可用” | 核对 Codex 版本、进程实际 `CODEX_HOME`、会话和 `goals_1.sqlite` 的存在与读权限;不手工补库或改状态。格式/schema/锁异常持续时在 `/hooks` 停用 guard |
96
+ | hook 超时/失败提示 | 本次检测失效,工具可能继续。检查 Python、文件读取与存储延迟,在隔离环境重验后再调整 3 秒上限 |
97
+ | 超过阈值但没有提示 | 核对当前根线程是否有 active goal、新统计是否已记录及工具事件覆盖;不能用 UI 百分比直接代替本地统计 |
98
+ | Stop 后仍 active 或持续续跑 | 在原对话 `/goal pause`;检查暂停授权和其他 Stop hooks。其他 hooks 的继续/停止要求须组合实测,不能宣称所有组合兼容 |
99
+
100
+ 临时停用:在 `/hooks` 中仅禁用本 guard 的两个 handler。永久移除:从两个事件数组中只删除 command 指向 `context-goal-guard.py` 的 handler,保留其他 hooks;确认不再引用后可删除该脚本副本。任务记录保留,paused goal 不自动恢复,不需要数据库迁移或回填。
101
+
102
+ ## 本地验证
103
+
104
+ ```sh
105
+ python3 -m unittest discover -s test/codex -p 'test_*.py'
106
+ python3 -m py_compile hooks/codex/context-goal-guard.py
107
+ ```
108
+
109
+ 行为测试通过真实脚本入口、临时 JSONL 和只读 SQLite 验证阈值、身份、无写入与异常降级;真实 Codex 部署和接续证据位于上述里程碑记录。fixture 不能替代真实阈值与调度验收。
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Read-only context reminder for an active /goal, verified against the Claude Code 2.1.288 transcript format.
4
+ // Never writes files and never exits with code 2: anything unexpected degrades to "detection unavailable".
5
+
6
+ const fs = require('node:fs');
7
+
8
+ // Percent of the window; the handler command may override it with `--threshold N`.
9
+ const DEFAULT_THRESHOLD = 70;
10
+ // Hook input and transcript carry no window size; only windows checked against statusLine are trusted.
11
+ const WINDOWS = {
12
+ 'claude-opus-5-5': 1000000,
13
+ 'claude-sonnet-5-5': 1000000,
14
+ 'claude-haiku-4-5-20251001': 200000,
15
+ };
16
+ const WINDOW_ENV = ['CLAUDE_CODE_DISABLE_1M_CONTEXT', 'DISABLE_COMPACT', 'CLAUDE_CODE_MAX_CONTEXT_TOKENS'];
17
+
18
+ class Unavailable extends Error {}
19
+
20
+ function readTranscript(file) {
21
+ if (typeof file !== 'string' || !file) throw new Unavailable('hook 输入缺少会话记录路径');
22
+ let text;
23
+ try {
24
+ text = fs.readFileSync(file, 'utf8');
25
+ } catch {
26
+ throw new Unavailable('无法读取会话记录');
27
+ }
28
+ let goal = null;
29
+ let usage = null;
30
+ for (const line of text.split('\n')) {
31
+ let record;
32
+ try {
33
+ record = JSON.parse(line);
34
+ } catch {
35
+ continue; // blank lines and the line still being written
36
+ }
37
+ if (!record || record.isSidechain === true) continue;
38
+ if (record.type === 'attachment' && record.attachment?.type === 'goal_status') goal = record.attachment;
39
+ else if (record.type === 'system' && record.subtype === 'compact_boundary') usage = null;
40
+ else if (record.type === 'assistant' && record.message?.usage && record.message.model !== '<synthetic>'
41
+ && !record.isApiErrorMessage) usage = record.message;
42
+ }
43
+ // Same rule Claude Code uses when resuming a goal.
44
+ return { active: !!goal && !goal.met && !goal.failed, message: usage };
45
+ }
46
+
47
+ function tokens(message) {
48
+ const { usage } = message;
49
+ const last = Array.isArray(usage.iterations) && usage.iterations.length ? usage.iterations.at(-1) : usage;
50
+ let used = 0;
51
+ for (const field of ['input_tokens', 'cache_creation_input_tokens', 'cache_read_input_tokens']) {
52
+ const value = last?.[field] ?? 0;
53
+ if (!Number.isInteger(value) || value < 0) throw new Unavailable('用量统计格式未知');
54
+ used += value;
55
+ }
56
+ return used;
57
+ }
58
+
59
+ function threshold(args) {
60
+ if (args.length === 0) return DEFAULT_THRESHOLD;
61
+ if (args.length === 2 && args[0] === '--threshold' && /^[1-9][0-9]?$/.test(args[1])) return Number(args[1]);
62
+ throw new Unavailable('阈值参数无效,应为 --threshold 加 1–99 的整数');
63
+ }
64
+
65
+ function contextWindow(model) {
66
+ const override = WINDOW_ENV.find((name) => process.env[name]);
67
+ if (override) throw new Unavailable(`设置了改变上下文窗口的环境变量 ${override}`);
68
+ if (!Object.hasOwn(WINDOWS, model)) throw new Unavailable(`模型 ${String(model)} 的上下文窗口未经验证`);
69
+ return WINDOWS[model];
70
+ }
71
+
72
+ function reminder(shown, limit) {
73
+ return `上下文估算使用率 ${shown}% 已超过 ${limit}%,当前 goal 仍 active。`
74
+ + '本提醒优先于继续推进 goal 的指令:即使收到「继续」类消息,也先完成收尾并结束本回合,新工作在新对话中进行。'
75
+ + '1) 停止开始新的实质任务;已启动的工作处理到可交接,不为收尾发起全量测试或额外研究。'
76
+ + '2) 按当前任务已有约定更新进度与记录:已完成、未完成、已验证、未验证、下一步;没有既有记录时在最终回复交接,不新建文件。'
77
+ + '3) 停止仍在运行的后台命令与子 agent(用户明确要求保留的除外),并在交接中列出;否则它们会在 goal 暂停后唤醒会话。'
78
+ + '4) 不要声称 goal 条件已满足或不可能,写明「因上下文收尾而暂停,工作未完成」。'
79
+ + '5) 完成后结束本回合,并告知用户:guard 会在回合结束时暂停 goal,请在同一项目目录新开对话或 /clear,'
80
+ + '提供计划路径或交接摘要,并重新设置 /goal。'
81
+ + '重复出现的本提醒属于同一次收尾,不重复追加记录。';
82
+ }
83
+
84
+ function backgroundList(tasks) {
85
+ const clip = (text) => (text.length > 200 ? `${text.slice(0, 200)}…` : text);
86
+ const items = tasks.map((task) => `${task?.id}(${clip(String(task?.command ?? task?.description ?? task?.type))})`);
87
+ return `仍在运行的后台任务:${items.join(';')}。请停止它们并在交接中列出。`;
88
+ }
89
+
90
+ function endTurn(shown, limit, background) {
91
+ return `上下文估算使用率 ${shown}% 已超过 ${limit}%,guard 已结束本回合,goal 暂停,工作未完成。`
92
+ + '请在同一项目目录新开对话(或 /clear),提供计划路径或交接摘要并说明继续执行;新对话不继承 goal,'
93
+ + '如需继续使用本 guard,请重新设置 /goal。不再需要原 goal 时可在本会话 /goal clear。'
94
+ + '如交接不完整,在本会话发一句「按上下文收尾提醒补交接」,回合结束后会再次暂停。'
95
+ + (background ? `仍有 ${background} 个后台任务在运行,它们结束或 check-in 时会短暂唤醒会话,guard 会再次结束回合。` : '');
96
+ }
97
+
98
+ function unavailable(reason) {
99
+ return {
100
+ systemMessage: `context goal guard 检测不可用:${reason};参见 docs/claude-context-goal-guard.md 排查,`
101
+ + '或从 .claude/settings.local.json 移除本 guard',
102
+ };
103
+ }
104
+
105
+ function decide(input) {
106
+ const event = input?.hook_event_name;
107
+ if ((event !== 'PostToolBatch' && event !== 'Stop') || input.agent_id) return null;
108
+ try {
109
+ const { active, message } = readTranscript(input.transcript_path);
110
+ if (!active || !message) return null;
111
+ const limit = threshold(process.argv.slice(2));
112
+ const used = tokens(message);
113
+ const window = contextWindow(message.model);
114
+ if (used * 100 <= window * limit) return null;
115
+ const shown = Math.round((used / window) * 100);
116
+ if (event === 'PostToolBatch') {
117
+ return { hookSpecificOutput: { hookEventName: event, additionalContext: reminder(shown, limit) } };
118
+ }
119
+ const background = Array.isArray(input.background_tasks) ? input.background_tasks : [];
120
+ if (input.stop_hook_active !== true) {
121
+ const reason = reminder(shown, limit) + (background.length ? backgroundList(background) : '')
122
+ + '若已完成收尾,只需一句确认后结束。';
123
+ return { decision: 'block', reason };
124
+ }
125
+ return { continue: false, stopReason: endTurn(shown, limit, background.length) };
126
+ } catch (error) {
127
+ // Warn once per stop instead of on every tool batch; never block or end the turn on uncertainty.
128
+ if (event !== 'Stop') return null;
129
+ return unavailable(error instanceof Unavailable ? error.message : '内部错误');
130
+ }
131
+ }
132
+
133
+ let output = null;
134
+ try {
135
+ output = decide(JSON.parse(fs.readFileSync(0, 'utf8')));
136
+ } catch {
137
+ output = null;
138
+ }
139
+ if (output) process.stdout.write(JSON.stringify(output));
@@ -0,0 +1,26 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolBatch": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-goal-guard.js\" --threshold 70",
9
+ "timeout": 5
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "Stop": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/context-goal-guard.js\" --threshold 70",
20
+ "timeout": 5
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only context reminder for the verified Codex 0.160.0 transcript format."""
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import re
8
+ import sqlite3
9
+ import sys
10
+ import uuid
11
+
12
+
13
+ # Percent of the window; the handler command may override it with `--threshold N`.
14
+ DEFAULT_THRESHOLD = 65
15
+
16
+ UNAVAILABLE = {
17
+ "systemMessage": "上下文 goal guard 检测不可用;请核对 CODEX_HOME、会话格式及随附的启用说明。"
18
+ "持续异常时请在 /hooks 中停用本 guard 并按说明排查。"
19
+ }
20
+
21
+
22
+ def root_transcript(data, home):
23
+ if data.get("agent_id"):
24
+ return None
25
+ session_id = data["session_id"]
26
+ if not isinstance(session_id, str):
27
+ raise ValueError("invalid session identity")
28
+ uuid.UUID(session_id)
29
+ path = Path(data["transcript_path"])
30
+ if not path.is_absolute() or not path.resolve().is_relative_to((home / "sessions").resolve()):
31
+ raise ValueError("transcript outside current home")
32
+ with path.open("rb") as stream:
33
+ header = stream.readline(1024 * 1024)
34
+ if not header.endswith(b"\n"):
35
+ raise ValueError("incomplete session metadata")
36
+ record = json.loads(header)
37
+ if record["type"] != "session_meta":
38
+ raise ValueError("missing session metadata")
39
+ meta = record["payload"]
40
+ if not isinstance(meta, dict):
41
+ raise ValueError("unknown session metadata")
42
+ source = meta.get("source")
43
+ if meta.get("parent_thread_id") or isinstance(source, dict) and "subagent" in source:
44
+ return None
45
+ if (meta["id"] != session_id or meta["session_id"] != session_id
46
+ or source not in ("cli", "exec", "vscode") or meta["cli_version"] != "0.160.0"):
47
+ raise ValueError("unverified identity or format")
48
+ return path
49
+
50
+
51
+ def goal_active(home, thread):
52
+ database = home / "goals_1.sqlite"
53
+ con = sqlite3.connect(database.resolve().as_uri() + "?mode=ro", uri=True, timeout=0.2)
54
+ try:
55
+ found = con.execute("SELECT status FROM thread_goals WHERE thread_id = ?", (thread,)).fetchone()
56
+ finally:
57
+ con.close()
58
+ if found is not None and not isinstance(found[0], str):
59
+ raise ValueError("unknown goal status format")
60
+ return found is not None and found[0] == "active"
61
+
62
+
63
+ def reverse_records(stream):
64
+ stream.seek(0, os.SEEK_END)
65
+ remaining = stream.tell()
66
+ pending = b""
67
+ discard_tail = True
68
+ while remaining:
69
+ size = min(65536, remaining)
70
+ remaining -= size
71
+ stream.seek(remaining)
72
+ parts = (stream.read(size) + pending).split(b"\n")
73
+ pending = parts.pop(0)
74
+ for line in reversed(parts):
75
+ # Drop the trailing fragment, or the empty line after a final newline.
76
+ if discard_tail:
77
+ discard_tail = False
78
+ elif line.strip():
79
+ yield json.loads(line)
80
+ if pending.strip() and not discard_tail:
81
+ yield json.loads(pending)
82
+
83
+
84
+ def context_usage(path, model):
85
+ if not isinstance(model, str) or not model:
86
+ raise ValueError("missing current model")
87
+ candidate = None
88
+ newer_context = False
89
+ windows = []
90
+ window_ids = []
91
+ with path.open("rb") as stream:
92
+ for record in reverse_records(stream):
93
+ kind = record["type"]
94
+ payload = record["payload"]
95
+ if not isinstance(payload, dict):
96
+ raise ValueError("unknown record payload")
97
+ if kind == "compacted" or (kind == "event_msg" and payload.get("type") in
98
+ ("context_compacted", "model_context_changed", "model_changed")):
99
+ if candidate is None or newer_context:
100
+ raise ValueError("statistics invalidated")
101
+ return candidate
102
+ if kind == "turn_context":
103
+ if payload["model"] != model:
104
+ raise ValueError("statistics belong to another model")
105
+ context = payload.get("context_window")
106
+ if context is not None:
107
+ identity = context["window_id"]
108
+ if not isinstance(identity, str) or not identity:
109
+ raise ValueError("unknown context identity")
110
+ window_ids.append(identity)
111
+ if "model_context_window" in payload:
112
+ windows.append(payload["model_context_window"])
113
+ if candidate is not None:
114
+ if any(type(w) is not int or w != candidate[1] for w in windows):
115
+ raise ValueError("context window changed")
116
+ if len(set(window_ids)) > 1 or window_ids and context is None:
117
+ raise ValueError("context identity changed")
118
+ return candidate
119
+ newer_context = True
120
+ elif kind == "event_msg" and payload.get("type") == "token_count" and candidate is None:
121
+ info = payload["info"]
122
+ used = info["last_token_usage"]["total_tokens"]
123
+ window = info["model_context_window"]
124
+ if type(used) is not int or used < 0 or type(window) is not int or window <= 0:
125
+ raise ValueError("invalid token statistics")
126
+ candidate = used, window
127
+ elif kind == "session_meta":
128
+ if candidate is not None and not newer_context:
129
+ return candidate
130
+ break
131
+ elif kind not in ("event_msg", "response_item", "token_usage_record", "world_state"):
132
+ raise ValueError("unknown record format")
133
+ raise ValueError("no current token statistics")
134
+
135
+
136
+ def threshold(args):
137
+ if not args:
138
+ return DEFAULT_THRESHOLD
139
+ if len(args) == 2 and args[0] == "--threshold" and re.fullmatch(r"[1-9][0-9]?", args[1]):
140
+ return int(args[1])
141
+ raise ValueError("invalid threshold argument")
142
+
143
+
144
+ def reminder(used, window, limit):
145
+ return (
146
+ f"本地上下文估算使用率 {used / window * 100:.2f}% 已严格超过 {limit}%,当前 goal 仍 active。"
147
+ "停止开始新的实质任务,先将已启动工作处理到可交接状态;沿用已有任务计划和记录更新进度,"
148
+ "写清已完成、未完成、已验证、未验证、下一步及仍在运行的进程。没有既有记录时在最终回复交接即可,"
149
+ f"记录写入失败也先在回复保存最小交接摘要。仅当用户对当前 goal 有显式的 {limit}% 暂停请求时,"
150
+ "按该请求使用原生 goal 工具设为 paused 并核对;不清楚则向用户确认并如实说明尚未暂停,提示 /goal pause。"
151
+ "hook 不构成暂停授权;后续 resume 撤销原请求。不要以 complete 代替暂停或直接写数据库。"
152
+ "然后报告真实状态、记录位置或交接摘要,提醒用户在同一工作目录自行新开对话继续,"
153
+ "新对话不会继承 goal,需用户手动设置 goal 并重新明确暂停策略。重复提醒视为同一次收尾。"
154
+ )
155
+
156
+
157
+ def run(data, args=()):
158
+ if not isinstance(data, dict):
159
+ raise ValueError("invalid hook input")
160
+ event = data["hook_event_name"]
161
+ if event not in ("PreToolUse", "Stop"):
162
+ return {}
163
+ if event == "Stop" and type(data["stop_hook_active"]) is not bool:
164
+ raise ValueError("invalid stop state")
165
+ home = Path(os.environ.get("CODEX_HOME", str(Path.home() / ".codex")))
166
+ if not home.is_absolute():
167
+ raise ValueError("unknown Codex home")
168
+ home = home.resolve()
169
+ path = root_transcript(data, home)
170
+ if path is None or not goal_active(home, data["session_id"]):
171
+ return {}
172
+ limit = threshold(args)
173
+ used, window = context_usage(path, data["model"])
174
+ if used * 100 <= window * limit:
175
+ return {}
176
+ if event == "PreToolUse":
177
+ return {"hookSpecificOutput": {"hookEventName": event, "additionalContext": reminder(used, window, limit)}}
178
+ if not data["stop_hook_active"]:
179
+ return {"decision": "block", "reason": reminder(used, window, limit)}
180
+ return {"systemMessage": f"上下文超过 {limit}%,goal 仍 active,收尾暂停尚未成功。"
181
+ "请在原对话执行 /goal pause,再按交接说明自行新开对话;此告警不停止 goal 自动续跑。"}
182
+
183
+
184
+ if __name__ == "__main__":
185
+ try:
186
+ result = run(json.load(sys.stdin), sys.argv[1:])
187
+ except (ValueError, TypeError, KeyError, OSError, sqlite3.Error):
188
+ result = UNAVAILABLE
189
+ print(json.dumps(result, ensure_ascii=False))
@@ -0,0 +1,26 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/context-goal-guard.py\" --threshold 65",
9
+ "timeout": 3
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "Stop": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/context-goal-guard.py\" --threshold 65",
20
+ "timeout": 3
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xgent-ai/skills",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "XGENT skills 与 Claude Code 辅助工具:通过 npx 为项目安装 hooks、启用 settings,并安装 vendor 的 impeccable",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -18,6 +18,12 @@
18
18
  "files": [
19
19
  "bin",
20
20
  ".claude/hooks",
21
+ "hooks/codex/context-goal-guard.py",
22
+ "hooks/codex/hooks.example.json",
23
+ "docs/codex-context-goal-guard.md",
24
+ "hooks/claude/context-goal-guard.js",
25
+ "hooks/claude/settings.example.json",
26
+ "docs/claude-context-goal-guard.md",
21
27
  "skills",
22
28
  "vendor/impeccable/bundle",
23
29
  "vendor/impeccable/VERSION.json",
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: agi-mode
3
- description: "以结果契约、主动实验、反证验证和可恢复执行推进复杂任务。用于 agi-mode、AGI 模式,或用户要求自主攻克难题、持续推进到可验证交付、失败后换策略继续解决的任务。可用于开发、调查、设计、优化与技能自进化;普通问答与局部小改动无需加载完整流程。"
3
+ argument-hint: "<任务描述> 或 复盘 / reflect [材料/范围/沉淀目标] 或 sdlc [流程/阶段/问题] 或 init-sdlc [PRD/计划/范围] 或 what-next [查询/更新] [PRD/计划/范围]"
4
+ description: "用于 AGI 模式、自主复杂任务、SDLC 流程指引、init-sdlc 初始化或 what-next 状态与续做;咨询查询只读,不自动实施。按阶段指导交接,以实验和证据推进交付。"
4
5
  disable-model-invocation: true
5
6
  ---
6
7
 
@@ -10,6 +11,28 @@ disable-model-invocation: true
10
11
 
11
12
  AGI 是这个工作模式的名字,不是能力声明。流程强度随不确定性、失败代价和任务跨度增加;简单任务直接完成并验证,不为运行本模式制造流程。
12
13
 
14
+ ## 快捷指令
15
+
16
+ `$agi-mode 复盘` 与 `$agi-mode reflect` 等价,直接读取并执行 [Reflection:证据复盘](playbooks/reflection.md)。默认回顾当前会话及已授权的经验记录,提炼踩坑教训和重复操作的自动化机会,给出沉淀建议;已有修改授权时直接落实。可在指令后补充材料路径、复盘范围或沉淀目标。
17
+
18
+ ## SDLC 咨询与阶段指引
19
+
20
+ 用户在任意阶段询问开发流程、产物分工、技能选择、交接条件或如何推进时,读取 [SDLC 指引](playbooks/sdlc.md)。支持 `$agi-mode sdlc [问题]`,也支持直接用自然语言提问,不要求记住子命令。概念咨询直接解释;涉及当前项目判断时由 what-next 核实相关事实,再给有依据的建议。只咨询不启动写作、评审、实施或状态初始化。
21
+
22
+ 明确要求执行的任务仍进入对应流程,已有执行授权继续有效;中途询问 SDLC 时简答后接续原任务。单独安装 agi-mode 即可获得全生命周期指引,专用技能按需读取,缺少时说明等价动作与限制。
23
+
24
+ ## 工作目录与状态入口
25
+
26
+ 需要持久状态时读 [状态目录约定](references/workspace-layout.md):开发生命周期状态保存在仓根 `.xgent-ai/sdlc/`,按类型和对象分开,链接对应原文及证据。该目录只放状态;PRD、开发计划、评审报告、实施记录等原产物继续按用户习惯和项目约定保存,不因启用模式改变位置或复制正文。
27
+
28
+ `.xgent-ai/` 下每个实际创建的目录都有 `index.md`;写入者按目录约定同步本层索引及入口变化的祖先,查询只报告缺失或陈旧,不自动修复。
29
+
30
+ 询问“当前项目状态 / 下一步 / what-next”或维护项目跟进表时,直接走 [项目状态与下一步](playbooks/what-next.md),只加载该流程及需要的目录/协议参考,不进入后面的实施闭环。默认查询只读;初始化/更新才保存按对象拆分的状态页。正在执行的已授权任务收到状态询问,回答后继续原任务,不重新请求开工。
31
+
32
+ `$agi-mode init-sdlc [PRD/计划/范围]` 是显式初始化入口,走同一流程的写入模式:按需建立台账并登记已核实的现有材料。重复调用沿用已有台账、补齐缺项,不重置历史。兼容 `what-next 初始化`;日常查询用 `what-next`,保存最新状态用 `what-next 更新`。
33
+
34
+ what-next 用固定状态码和证据质量表达判断,主动发现关联评审与处置,核对原计划状态是否滞后;自愈按 [状态模型](assets/sdlc/state-model.md) 修复跟进,不从文件名推导评审通过或开发完成。
35
+
13
36
  ## Principles
14
37
 
15
38
  原则负责决定取舍,playbook 负责执行顺序。遇到下表的触发条件时读取对应原则全文,再作相关决定;playbook 内的原则入口是提示,不要求每次全部加载。原则文件是本 skill 的内部资源,不作为独立全局 skill 注册。
@@ -61,6 +84,8 @@ AGI 是这个工作模式的名字,不是能力声明。流程强度随不确
61
84
 
62
85
  | 当前任务 | Playbook | 完成标志 |
63
86
  | --- | --- | --- |
87
+ | SDLC 知识、PRD/CRD/计划/测试分工、阶段交接、技能选择或流程建议 | [SDLC 指引](playbooks/sdlc.md) | 有依据的阶段解释、下一步输入/动作/判据;咨询保持只读 |
88
+ | init-sdlc 初始化、项目状态、what-next、多计划进度、跨机盘点或维护跟进表 | [项目状态与下一步](playbooks/what-next.md) | 有来源的分类状态、冲突/同步边界与具体下一步;仅写入模式维护台账 |
64
89
  | 解释机制、只读调查、比较方案 | [调查与诊断](playbooks/investigation.md) | 有出处的答案、因果证据或明确的未知 |
65
90
  | 活进程异常、CPU / heap / trace / spindump 取证 | [运行时与离线取证](playbooks/forensics.md) | 从原始信号到代码位置的可追溯诊断 |
66
91
  | 设计方案、实施计划、用原型决定方向 | [设计与原型](playbooks/design-prototype.md) | 可实施的决策或已验证的原型结论 |
@@ -70,7 +95,7 @@ AGI 是这个工作模式的名字,不是能力声明。流程强度随不确
70
95
  | 比较 skill、prompt、模型、工具配置或执行流程 | [Eval:可信评测](playbooks/eval.md) | 隔离、可比的真实结果支持采用、拒绝或证据不足 |
71
96
  | 持续改进代码、指标或 agent 行为,autoresearch | [Hillclimb:持续进化](playbooks/hillclimb.md) | 实验记录完整,最佳候选经最终确认,目标状态明确 |
72
97
  | 比较多种结构并合成产物,arena | [Arena:多候选比较](playbooks/arena.md) | 匿名比较有依据,合成版本重新验证 |
73
- | 从会话提炼教训,reflect | [Reflection:证据复盘](playbooks/reflection.md) | 有出处的改进假设,按授权落实并标明验证等级 |
98
+ | 从会话或经验记录提炼教训,复盘 / reflect | [Reflection:证据复盘](playbooks/reflection.md) | 有出处的改进假设,按授权落实并标明验证等级 |
74
99
  | 行为不变的重构、接口或数据迁移 | [重构与迁移](playbooks/migration-refactoring.md) | 行为约束成立,调用方和数据状态完成核对 |
75
100
  | 页面、交互、视觉还原或跨端适配 | [界面交付](playbooks/ui-delivery.md) | 实际界面与交互证据符合目标 |
76
101
  | 审查代码、方案或外部审查意见 | [评审与处置](playbooks/review.md) | 有证据的发现及处置,不把意见数量当成果 |
@@ -0,0 +1,18 @@
1
+ # <目录名称> 索引
2
+
3
+ 用途:<本目录保存什么状态;原文和证据仍在用户约定位置。>
4
+ 覆盖:本目录直接文件与子目录;不含本索引自身。<分片时列明范围与全部分片入口。>
5
+
6
+ | 文件 / 子目录 | 用途 / 对象 ID | 入口 |
7
+ | --- | --- | --- |
8
+
9
+ <!-- 按相对路径稳定排序;文件链到自身,子目录链到其 index.md。事件叶目录一事件一行,列 event_id 与 facet/scope,不复制结论。只填真实条目,空目录写“暂无登记”。 -->
10
+
11
+ ## 维护约定
12
+
13
+ - 改变本目录内容的执行者负责维护:先保存状态文件,再更新本索引;创建子目录时同时创建其索引。父索引仅在入口增删、移动或说明变化时更新,无变化不重写或刷新时间。
14
+ - 索引只作导航;状态内容更新不必改索引。归档须有依据并保留入口,移动保持对象 ID;缺失目标先标待核对,不因当前机器缺文件删除记录或历史。索引维护不授权搬迁、删除原文。
15
+ - 合并时按 ID 与路径核对本地实际文件,保留双方新增项及未解决冲突,不按时间覆盖;修补漏项、重复和断链,保留人工说明。查询只报告问题,维护模式才修复。
16
+ - 新增归档/分片目录同样有 index.md;大索引用 index-<分片名>.md 分片,首页列全范围,每项恰属一个分片。index.md 与 index-*.md 不作为事件读取或计数。
17
+
18
+ <!-- SDLC 目录另链接仓内 protocol.md 的“目录索引维护”一节,按本文件位置计算相对路径;不保留安装机器路径。非开发任务保留上述自包含规则即可。 -->
@@ -0,0 +1,33 @@
1
+ ---
2
+ schema_version: 3
3
+ event_id: "<新 UUID,与文件名相同>"
4
+ object_id: "<沿用对象 UUID;新对象才生成>"
5
+ kind: "<prd / plan / review>"
6
+ title: "<可读标题>"
7
+ artifact: "<仓根相对路径或稳定 URL>"
8
+ facet: "<document / readiness / implementation / review / disposition 等>"
9
+ scope: "<稳定范围,如 plan、M2、F-01>"
10
+ state: "<state-model.md 对应 kind/facet 的固定状态码,不能确认用 unknown>"
11
+ source_state: null
12
+ evidence_status: "<confirmed / provisional / stale / conflict / unknown>"
13
+ observed_at: "<ISO 8601,带时区>"
14
+ predecessors: []
15
+ baseline:
16
+ head: "<完整 SHA 或 unknown>"
17
+ branch: "<分支、detached 或 unknown>"
18
+ worktree: "<clean / dirty / unknown>"
19
+ sources:
20
+ - path: "<仓根相对路径或稳定 URL>"
21
+ revision: "<Git 版本、sha256:内容指纹或 unknown>"
22
+ anchor: "<稳定章节/检查/问题定位>"
23
+ relations: []
24
+ ---
25
+
26
+ # <本次事实摘要>
27
+
28
+ - 事实与证据:<本次读到/验证了什么;评审写种类、所审对象版本和范围,区别于观察 checkout>
29
+ - 校准依据:<规范状态与来源自述的差异、证据覆盖与原文滞后;一致时简述依据>
30
+ - 未完项与限制:<缺证、未同步、待决;没有则写无,不填猜测>
31
+ - 下一步及解除条件:<动作 + 可观察完成判据;无后续则说明>
32
+
33
+ <!-- 创建事件时替换占位并移除此注释。source_state 有原文状态则保留其文字,否则 null;可选 candidate_state 只能写模型中的对应枚举。kind=review 时补 review_type: prd / dev-plan / code / other。sources.path、artifact 均按仓根解析;正文 Markdown 链接按本文件目录解析。relations 如 {type: covers, target: 对象UUID, scope: FR-1};评审对所审对象用 reviews,复审对前轮用 rechecks。 -->
@@ -0,0 +1,17 @@
1
+ # SDLC 工作导航
2
+
3
+ 本页列直接文件、子目录入口与当前重点,不自列。本目录只保存按对象拆分的状态;PRD、计划、报告及证据保留在用户原有位置,通过来源链接读取。
4
+
5
+ - 回写规则:[协议](protocol.md)
6
+ - 状态定义与自愈:[固定状态模型](state-model.md)
7
+ - 记录格式:[事件模板](event-template.md)
8
+ - 对象状态:[类型导航](status/index.md)
9
+ - 状态历史:[事件导航](events/index.md)
10
+
11
+ 由改变本目录内容的执行者按[目录索引维护规则](protocol.md#目录索引维护)同步入口;下级状态变化不逐级重写本页。分类或对象快捷链接可按需放在当前重点,不代替完整目录入口。
12
+
13
+ ## 当前重点
14
+
15
+ <只链接当前需要跟进的对象状态页,说明首选下一步与全局阻塞;完整活动项见分类索引。没有可确认活动项时如实说明。>
16
+
17
+ <!-- 初始化时只链接实际存在的索引;未创建的类别写“暂无登记”,不要留断链。 -->