@openprd/cli 0.1.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.
- package/.openprd/README.md +82 -0
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
- package/.openprd/benchmarks/index.md +37 -0
- package/.openprd/benchmarks/sources.yaml +56 -0
- package/.openprd/config.yaml +50 -0
- package/.openprd/discovery/config.json +21 -0
- package/.openprd/engagements/active/flows.md +30 -0
- package/.openprd/engagements/active/handoff.md +9 -0
- package/.openprd/engagements/active/intake.md +15 -0
- package/.openprd/engagements/active/prd.md +161 -0
- package/.openprd/engagements/active/review.html +61 -0
- package/.openprd/engagements/active/roles.md +21 -0
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/index.json +7 -0
- package/.openprd/quality/config.json +229 -0
- package/.openprd/reviews/v0001.html +1256 -0
- package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
- package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
- package/.openprd/schema/prd.schema.yaml +121 -0
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/standards/config.json +88 -0
- package/.openprd/standards/file-manual-template.md +28 -0
- package/.openprd/standards/folder-readme-template.md +28 -0
- package/.openprd/state/.gitkeep +0 -0
- package/.openprd/state/changes.json +12 -0
- package/.openprd/state/current.json +169 -0
- package/.openprd/state/version-index.json +15 -0
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +121 -0
- package/.openprd/state/versions/v0001.md +161 -0
- package/.openprd/templates/agent/intake.md +6 -0
- package/.openprd/templates/agent/prd.md +21 -0
- package/.openprd/templates/b2b/intake.md +6 -0
- package/.openprd/templates/b2b/prd.md +24 -0
- package/.openprd/templates/base/intake.md +18 -0
- package/.openprd/templates/base/prd.md +67 -0
- package/.openprd/templates/company/README.md +10 -0
- package/.openprd/templates/consumer/intake.md +6 -0
- package/.openprd/templates/consumer/prd.md +19 -0
- package/.openprd/templates/diagram/architecture.contract.json +53 -0
- package/.openprd/templates/diagram/product-flow.contract.json +76 -0
- package/.openprd/templates/industry/README.md +16 -0
- package/.openprd/templates/manifest.yaml +27 -0
- package/.openprd/templates/project/README.md +14 -0
- package/.openprd/templates/session/README.md +14 -0
- package/AGENTS.md +44 -0
- package/CONTRIBUTING.md +30 -0
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/README_CN.md +583 -0
- package/SECURITY.md +23 -0
- package/bin/openprd.js +5 -0
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +114 -0
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
- package/package.json +59 -0
- package/scripts/openprd-dev-check.mjs +5 -0
- package/scripts/openprd-review-presentation.mjs +82 -0
- package/skills/openprd-benchmark-router/SKILL.md +92 -0
- package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
- package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
- package/skills/openprd-diagram-review/SKILL.md +91 -0
- package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
- package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
- package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
- package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
- package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
- package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
- package/skills/openprd-discovery-loop/SKILL.md +196 -0
- package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
- package/skills/openprd-harness/SKILL.md +179 -0
- package/skills/openprd-harness/agents/openai.yaml +4 -0
- package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
- package/skills/openprd-harness/references/command-map.md +71 -0
- package/skills/openprd-harness/references/examples.md +26 -0
- package/skills/openprd-harness/references/usage-guide.md +335 -0
- package/skills/openprd-harness/references/workflow-gates.md +51 -0
- package/skills/openprd-learning-review/SKILL.md +75 -0
- package/skills/openprd-learning-review/agents/openai.yaml +4 -0
- package/skills/openprd-learning-review/references/content-contract.md +125 -0
- package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
- package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
- package/skills/openprd-learning-review/references/genre-library.md +43 -0
- package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
- package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
- package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
- package/skills/openprd-quality/SKILL.md +101 -0
- package/skills/openprd-requirement-intake/SKILL.md +76 -0
- package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
- package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
- package/skills/openprd-router/SKILL.md +40 -0
- package/skills/openprd-shared/SKILL.md +142 -0
- package/skills/openprd-shared/agents/openai.yaml +4 -0
- package/skills/openprd-shared/references/language-and-review.md +50 -0
- package/skills/openprd-shared/references/operating-rules.md +65 -0
- package/skills/openprd-shared/references/skill-architecture.md +70 -0
- package/skills/openprd-standards/SKILL.md +79 -0
- package/skills/openprd-standards/agents/openai.yaml +4 -0
- package/src/agent-integration.js +1717 -0
- package/src/benchmark.js +873 -0
- package/src/cli/args.js +460 -0
- package/src/cli/print.js +1423 -0
- package/src/codex-hook-runner-template.mjs +2422 -0
- package/src/dev-standards.js +372 -0
- package/src/diagram-core.js +1047 -0
- package/src/diagram-workspace.js +262 -0
- package/src/discovery.js +709 -0
- package/src/fleet.js +531 -0
- package/src/fs-utils.js +83 -0
- package/src/growth.js +545 -0
- package/src/html-artifacts.js +3803 -0
- package/src/knowledge.js +668 -0
- package/src/language-policy.js +142 -0
- package/src/learning-review.js +1655 -0
- package/src/loop.js +1290 -0
- package/src/openprd.js +1136 -0
- package/src/openspec/change-lifecycle.js +359 -0
- package/src/openspec/change-validate.js +248 -0
- package/src/openspec/constants.js +12 -0
- package/src/openspec/execute.js +300 -0
- package/src/openspec/generate.js +692 -0
- package/src/openspec/paths.js +111 -0
- package/src/openspec/tasks.js +352 -0
- package/src/prd-core.js +656 -0
- package/src/quality-html-artifact.js +1414 -0
- package/src/quality-learning.js +658 -0
- package/src/quality.js +1262 -0
- package/src/review-presentation.js +240 -0
- package/src/run-harness.js +1470 -0
- package/src/self-update.js +329 -0
- package/src/session-binding.js +140 -0
- package/src/source-inventory.js +224 -0
- package/src/standards.js +914 -0
- package/src/time.js +33 -0
- package/src/visual-compare.js +216 -0
- package/src/work-unit-migration.js +232 -0
- package/src/work-unit.js +88 -0
- package/src/workspace-core.js +1706 -0
- package/src/workspace-registry.js +162 -0
- package/src/workspace-workflow.js +1797 -0
|
@@ -0,0 +1,1717 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { renderApprovedBenchmarkRegistrySection } from './benchmark.js';
|
|
9
|
+
import { timestamp } from './time.js';
|
|
10
|
+
import { upsertWorkspaceRegistryEntry } from './workspace-registry.js';
|
|
11
|
+
|
|
12
|
+
const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
13
|
+
const OPENPRD_AGENT_TOOLS = ['codex', 'claude', 'cursor'];
|
|
14
|
+
const OPENPRD_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'];
|
|
15
|
+
const OPENPRD_HOOK_PROFILES = {
|
|
16
|
+
lite: ['UserPromptSubmit', 'PreToolUse', 'Stop'],
|
|
17
|
+
guarded: ['UserPromptSubmit', 'PreToolUse', 'Stop'],
|
|
18
|
+
full: OPENPRD_EVENTS,
|
|
19
|
+
};
|
|
20
|
+
const OPENPRD_DEFAULT_HOOK_PROFILE = 'lite';
|
|
21
|
+
const OPENPRD_HOOK_EVENTS_WITH_MATCHER = new Set(['PreToolUse', 'PostToolUse']);
|
|
22
|
+
const OPENPRD_LITE_WRITE_TOOL_MATCHER = '^(apply_patch|Write|Edit)$';
|
|
23
|
+
const OPENPRD_GUARDED_WRITE_TOOL_MATCHER = '^(Bash|apply_patch|Write|Edit)$';
|
|
24
|
+
const OPENPRD_HARNESS_DIR = cjoin('.openprd', 'harness');
|
|
25
|
+
const OPENPRD_HARNESS_EVENTS = cjoin(OPENPRD_HARNESS_DIR, 'events.jsonl');
|
|
26
|
+
const OPENPRD_HARNESS_HOOK_STATE = cjoin(OPENPRD_HARNESS_DIR, 'hook-state.json');
|
|
27
|
+
const OPENPRD_HARNESS_MANIFEST = cjoin(OPENPRD_HARNESS_DIR, 'install-manifest.json');
|
|
28
|
+
const OPENPRD_HARNESS_DRIFT = cjoin(OPENPRD_HARNESS_DIR, 'drift-report.json');
|
|
29
|
+
const LEGACY_CODEX_HOOK_OUTPUT_FIELDS = ['should_stop', 'additional_contexts', 'should_block', 'block_reason'];
|
|
30
|
+
const OPENPRD_GENERATED_MARKER = 'OPENPRD:GENERATED';
|
|
31
|
+
|
|
32
|
+
const CANONICAL_SKILLS = [
|
|
33
|
+
{
|
|
34
|
+
id: 'openprd-router',
|
|
35
|
+
description: 'OpenPrd 入口路由 skill:先判断当前任务该读哪个 skill、哪个命令面和哪个门禁。',
|
|
36
|
+
body: [
|
|
37
|
+
'# OpenPrd Router',
|
|
38
|
+
'',
|
|
39
|
+
'把这份 skill 当成 OpenPrd 的入口路由,而不是长文规则仓库。',
|
|
40
|
+
'',
|
|
41
|
+
'## 先做什么',
|
|
42
|
+
'',
|
|
43
|
+
'1. 先读 `.openprd/` 当前状态,并把 `openprd run . --context` 当作建议上下文,而不是自动执行指令。',
|
|
44
|
+
'2. 需要具体命令时,优先读取 `.openprd/harness/command-catalog.md`,不要把命令清单继续塞回 `AGENTS.md`。',
|
|
45
|
+
'3. 需要共用约束时,读 `$openprd-shared`;需要主工作流时,读 `$openprd-harness`。',
|
|
46
|
+
'',
|
|
47
|
+
'## 路由表',
|
|
48
|
+
'',
|
|
49
|
+
'- 需求入口分流、L0/L1/L2 判断、PRD lens 选择:`$openprd-requirement-intake`',
|
|
50
|
+
'- 主工作流、review/change/tasks、`run/loop`:`$openprd-harness`',
|
|
51
|
+
'- 最佳实践、benchmark、公开 GitHub 仓库、第三方技术事实、prompt/context engineering:`$openprd-benchmark-router`',
|
|
52
|
+
'- `docs/basic/`、文件说明书、文件夹 README、文档标准:`$openprd-standards`',
|
|
53
|
+
'- 就绪验证、EVO 门禁、HTML 质量评估报告、项目经验沉淀:`$openprd-quality`',
|
|
54
|
+
'- 架构图、产品流程图、可视化评审:`$openprd-diagram-review`',
|
|
55
|
+
'- 长时间只读挖掘、参考项目持续调研、requirements/specs/tasks 补全:`$openprd-discovery-loop`',
|
|
56
|
+
'- 学习包、归档阅读器、知识整理:`$openprd-learning-review`',
|
|
57
|
+
'',
|
|
58
|
+
'## 路由原则',
|
|
59
|
+
'',
|
|
60
|
+
'- `AGENTS.md` 只保留轻量入口合同;详细规则放进 repo-local skills、`.openprd/harness/command-catalog.md` 和 hooks。',
|
|
61
|
+
'- 公开 GitHub 仓库架构/对标先 DeepWiki;第三方库、API、SDK、MCP、CLI 用法先查本地证据,本地不足时再按 `resolve_library_id -> query_docs` 使用 Context7。',
|
|
62
|
+
'- hooks 已经强制处理 requirement / research / secrets / skill-visualization / weapp / browser / copy 这些门禁;不要再把它们膨胀回 `AGENTS.md` 静态长文。',
|
|
63
|
+
'- 不要用固定关键词决定是否写 PRD;先让 `$openprd-requirement-intake` 按影响面、未知数、决策成本和验证成本做语义分流。',
|
|
64
|
+
'',
|
|
65
|
+
].join('\n'),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'openprd-requirement-intake',
|
|
69
|
+
sourceSkill: 'openprd-requirement-intake',
|
|
70
|
+
description: 'OpenPrd 需求入口与 PRD 分流 skill:判断 L0/L1/L2,决定直接澄清、mini-plan 或正式 PRD,并选择 base/consumer/b2b/agent PRD lens。',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'openprd-shared',
|
|
74
|
+
description: 'OpenPrd 工作区、语言规则、门禁和 workspace-first 推理的共用守则。',
|
|
75
|
+
body: [
|
|
76
|
+
'# OpenPrd Shared',
|
|
77
|
+
'',
|
|
78
|
+
'这份规则集适用于所有 OpenPrd 工作。',
|
|
79
|
+
'',
|
|
80
|
+
'## 优先读取',
|
|
81
|
+
'',
|
|
82
|
+
'- `.openprd/state/current.json`',
|
|
83
|
+
'- `.openprd/state/task-graph.json`',
|
|
84
|
+
'- `.openprd/harness/install-manifest.json`',
|
|
85
|
+
'- `.openprd/harness/hook-state.json`',
|
|
86
|
+
'- `docs/basic/`',
|
|
87
|
+
'',
|
|
88
|
+
'## 运行规则',
|
|
89
|
+
'',
|
|
90
|
+
'- 动手前先从 `.openprd/` 重建上下文。',
|
|
91
|
+
'- 选择写入命令前,优先运行 `openprd status .` 和 `openprd next .`。',
|
|
92
|
+
'- 用户可见文档、进度日志、proposal、prompt 和报告默认使用简体中文;只保留必要专有名词、命令名、路径、字段名和 API 术语。',
|
|
93
|
+
'- 当 `locale` 为 `zh-CN` 时,diagram contract 中所有可见字段都必须使用简体中文;面向用户的 review.html 或 diagram HTML 文案不要使用 `freeze` 这类内部流程词,改写为“需求定稿前”“进入实现前确认”等业务可理解表达。',
|
|
94
|
+
'- OpenPrd 用户默认懂业务和产品,但不想读技术黑话;对外输出先给结论和下一步,能一句讲清楚就不要拆成两步。',
|
|
95
|
+
'- 主动替用户补全范围边界、失败路径、恢复路径、实现成本、维护成本、滥用风险和第三方依赖;默认按性价比选方案。',
|
|
96
|
+
'- 涉及第三方 API、模型、云服务或付费工具时,用表格比较效果、价格、接入成本、限制、风险和推荐理由;用户明确质量优先时,提高质量和稳定性权重。',
|
|
97
|
+
'- 当用户的问题包含多个对象、方案、文件、场景、风险、验证项、素材或任务,并且需要同时呈现状态、证据、影响、动作或推荐时,Agent 应主动使用 Markdown 表格,不等用户要求。先用一句话给结论,再给表格。',
|
|
98
|
+
'- 表格优先用于方案对比、状态盘点、问题排查、风险审查、多对象 QA、文件/命令清单、需求场景覆盖和内容/素材规划;单一结论、单一动作、代码示例、命令示例和叙事型说明不要强行表格化。',
|
|
99
|
+
'- 面向用户的时间统一使用上海时区 `YYYY-MM-DD HH:mm:ss` 格式,不带 `T`、`Z` 或毫秒。',
|
|
100
|
+
'- 保持未解决假设可见,不要悄悄补脑。',
|
|
101
|
+
'- 项目基线文档路径只能是 `docs/basic/`。',
|
|
102
|
+
'- 声称就绪前,至少通过 `openprd validate .` 和 `openprd standards . --verify`。',
|
|
103
|
+
'- 实现就绪还要运行 `openprd quality . --verify`,并审阅 HTML 质量评估报告中的场景标签、必需 EVO 门禁、可观测性、业务护栏、评估执行环境、性能和知识缺口。',
|
|
104
|
+
'- 用户要求生成图片、封面图、配图、海报、插画、图标、贴纸、头像、banner、主视觉/KV、运营图、效果图、视觉稿、mockup、先看样子或先确认设计方向时,默认直接调用 Codex 原生 Image 2 生图能力产出图片;对 logo、icon、avatar、badge 等开发素材,如果用户未明确要求 mockup、场景图、设备框、卡片承载、名片/包装展示或参考界面复刻,默认按独立素材输出(standalone asset)处理:使用全画布单主体,不额外添加 UI frame、卡片、设备壳、名片、桌面陈列、手持实拍或其他展示容器。只有当用户明确要求 mockup、场景化效果图、容器化呈现,或参考图本身包含这些结构时,才生成对应容器或场景;除非用户明确指定 HTML、SVG、CSS、Canvas、代码稿或可编辑矢量/source artifact,不要改用临时 HTML/SVG/CSS 再截图。',
|
|
105
|
+
'- OpenPrd 的 `review.html` 用于需求评审,不能替代图片或效果图生成;`visual-compare` 只用于实现阶段已有参考图之后的实现截图对比。',
|
|
106
|
+
'- 界面、页面、视觉、样式或前端体验开发中,只要已经有效果图、设计稿、图片资产或用户给图并进入实现阶段,阶段性完成后必须先截实现图,再运行 `openprd visual-compare . --reference <效果图> --actual <实现截图>` 生成左右对比 JPG。左侧标注“效果图”,右侧标注“实现截图”;Agent 必须查看合成图并继续对标,直到没有明显视觉差异,不能只凭主观判断宣称完成。',
|
|
107
|
+
'- 看到生成文件疑似过期时,先运行 `openprd doctor .`。',
|
|
108
|
+
'- `openprd run . --context` 只是建议。规划、分析、review、影响范围说明等请求保持只读,除非当前用户消息明确要求开发、实现、继续任务、深度调研、对标复刻或 commit/push。',
|
|
109
|
+
'- 用户给出会话 ID 并要求继续时,按工具无关的历史会话精确续接;不要要求或使用工具专属 ID;当前 active change、相似历史或 requirement gate 只能作为背景,不能替代该会话 ID。',
|
|
110
|
+
'- 代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 `openprd dev-check . <file...>` 或 `node scripts/openprd-dev-check.mjs . <file...>`;700 行以内正常,701-1500 行需说明局部职责,超过 1500 行要判断本轮是否扩大职责,扩大则先重构/拆分/解耦并复查,窄 bugfix 或小修暂不拆时说明原因和后续拆分建议。',
|
|
111
|
+
'- 执行中发现可沉淀项时,不要中途打断当前任务:高置信工具识别补全和减少重复打扰这类低风险项可自动补齐;用户偏好、项目协作规矩和 OpenPrd 默认行为先沉淀为 `.openprd/growth` 候选,收工时再集中运行 `openprd grow . --review` 请用户确认。',
|
|
112
|
+
'- 维护 OpenPrd 本身时,只要新增或修改配置类能力(阈值、规则、识别、豁免、命令别名、环境差异、用户偏好或策略开关),都要做 grow-aware 自检:高置信应可成长时默认纳入 `openprd grow`;不确定时主动问用户;明确一次性或固定规则时才保持静态配置。',
|
|
113
|
+
'- 只要实现新增或修改文件,就做文档影响检查;缺失的 `docs/basic/`、文件说明书和文件夹 README 要补齐,已有文档受影响时要更新。',
|
|
114
|
+
'- 涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:检查命令入口、参数、输出契约、`help`、`doctor`、`dry-run`、`status` 与接口协议、返回结构、身份边界是否受影响,并同步更新 `docs/basic/backend-structure.md`;若某一面不适用也要明确写原因。',
|
|
115
|
+
'- Codex hooks 默认使用 `lite`:`UserPromptSubmit` 注入上下文、轻量 `PreToolUse` 写入门禁,以及 `Stop` 本轮收工回顾。只有项目明确需要更重的工具级遥测时,才切到 `full`。',
|
|
116
|
+
'- 需求复杂度分流优先使用 `$openprd-requirement-intake`,不要按固定关键词判断:L0 小修直接处理并事后说明;L1 中等改动先给对话内 mini-plan 再执行;L2 高影响或边界不清的需求在改代码前必须先完成需求入口:clarify、评审、任务拆解。`review --mark confirmed` 只记录稳定评审稿确认;如果用户原始意图已经明确要求实现,tasks 就绪后可直接进入执行,否则等待一句明确的执行指令。',
|
|
117
|
+
'- 涉及最佳实践、benchmark、对标、参考产品、prompt engineering、Agent harness、context engineering、图标资源、CLI 或 skill 体系设计时,先使用 `$openprd-benchmark-router` 选择证据源,再进入 Context7、DeepWiki 或官方资料调研。',
|
|
118
|
+
'- 入口路由优先看 `$openprd-router`;具体命令速查优先看 `.openprd/harness/command-catalog.md`。',
|
|
119
|
+
'- `AGENTS.md` 只保留轻量合同;详细执行细则优先沉淀到 repo-local skills、command catalog 和 hooks。',
|
|
120
|
+
'- 任务需要 API key、token、账号信息、第三方服务凭证或个人信息时,先使用 `secrets-vault` skill,且不要直接读取原始 vault 文件。',
|
|
121
|
+
'- 修改 skill、`SKILL.md`、`AGENTS.md` 或相关 workflow 前,先读取现状、输出彩色 Mermaid 方案图,并等待用户确认后再编辑相关文件。',
|
|
122
|
+
'- 涉及微信小程序测试、验证、截图、日志、网络请求、开发者工具自动化或运行态相关改动时,先使用 `weapp-dev-mcp` skill;未通过本地 MCP 实际验证时,不要宣称“小程序已验证”。',
|
|
123
|
+
'- 用户明确要求 Computer Use 时优先使用 Computer Use,并尽量在 Codex-owned browser window 中操作;对提交、删除、发送、切换账号、退出登录、支付、关闭标签页等高风险网页动作先确认窗口归属。',
|
|
124
|
+
'- 修改用户可见文案前,先检查 `i18n`、`locales`、`translations`、`Localizable` 或其他语言资源;若项目已有多语言结构,用户可见文案要同步维护到所有已支持语言,并避免暴露 API、SDK、模型、数据库、缓存或错误码等实现细节。',
|
|
125
|
+
'',
|
|
126
|
+
'## 写入纪律',
|
|
127
|
+
'',
|
|
128
|
+
'- 只读命令优先:`status`、`next`、`validate`、`standards --verify`、`doctor`。',
|
|
129
|
+
'- 下一道门禁没看清之前,不要贸然执行写入命令。',
|
|
130
|
+
'- 面对规划、分析、审查类请求,不要运行 `openprd loop --run`、`openprd tasks --advance`、`openprd discovery --advance`、`openprd loop --finish --commit`、git commit 或 git push。',
|
|
131
|
+
'- 代码改动完成后,要回顾 `openprd dev-check` 输出;若出现 `attention` 或 `warning`,说明是否已局部处理、是否已拆分,或为什么窄修暂不拆。',
|
|
132
|
+
'- 代码改动完成后,要回顾自我成长项:已自动补齐的低风险工具识别项简短说明;仍待确认的偏好、项目规矩或 OpenPrd 默认行为再用 `openprd grow . --review` 集中呈现。',
|
|
133
|
+
'- 代码改动完成后,要说明 `docs/basic/`、文件说明书和文件夹 README 是新增、更新还是有意不变。',
|
|
134
|
+
'- 用户要求生成图片、封面图、配图、海报、插画、图标、贴纸、头像、banner、主视觉/KV、运营图、效果图、视觉稿、mockup、先看样子或先确认设计方向时,最终回复应给出 Image 2 生成的图片结果;如果是 logo、icon、avatar、badge 等开发素材且用户未明确要求 mockup 或场景化呈现,默认给出独立素材输出结果。只有实现阶段已有参考图时,才给出 `openprd visual-compare` 生成的 JPG 路径并说明对比后是否仍有差异。',
|
|
135
|
+
'- `freeze`、`handoff`、`change --apply`、`change --archive`、commit、push、release、publish 等高风险动作都要求前置门禁全绿。',
|
|
136
|
+
'',
|
|
137
|
+
'## 修复路径',
|
|
138
|
+
'',
|
|
139
|
+
'1. 运行 `openprd doctor .`。',
|
|
140
|
+
'2. 如果生成引导或 hooks 漂移,运行 `openprd update .`。',
|
|
141
|
+
'3. 运行 `openprd standards . --verify` 并修复文档标准。',
|
|
142
|
+
'4. 运行 `openprd quality . --verify` 并审阅 HTML 质量评估报告;若 `productionReady=false`,最终回复必须列出缺证据或需关注的必需 EVO 门禁。',
|
|
143
|
+
'5. 报告就绪前运行 `openprd validate .`。',
|
|
144
|
+
'',
|
|
145
|
+
].join('\n'),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'openprd-benchmark-router',
|
|
149
|
+
description: '为 OpenPrd 产品、CLI、Agent harness、AI code review / PR review harness、上下文工程、提示词优化和图标资源选择对标来源与调研路径。',
|
|
150
|
+
body: [
|
|
151
|
+
'# OpenPrd Benchmark Router',
|
|
152
|
+
'',
|
|
153
|
+
'当用户要求最佳实践、benchmark、对标、参考设计、产品优化、CLI 优化、Agent harness 优化、AI code review / PR review harness、上下文工程、提示词工程或图标资源时,先使用这份 skill。',
|
|
154
|
+
'',
|
|
155
|
+
'## 核心原则',
|
|
156
|
+
'',
|
|
157
|
+
'- 不把对标当成固定关键词匹配。结合用户目标判断参考源是否真的能提升设计、实现、排查、评审、规划或文档质量。',
|
|
158
|
+
'- 不强行对标。环境、权限、账号、普通脚本报错、一次性短问答或与产品/领域设计无关的问题,继续当前任务即可。',
|
|
159
|
+
'- 不默认下载全文、仓库或整站。先保留轻量链接、来源 ID 和适用边界;真正需要事实时再读取。',
|
|
160
|
+
'- 通常只选 1-3 个最相关来源;不要为了显得全面而扩大上下文。',
|
|
161
|
+
'- 不把索引、来源目录或记忆当事实来源;未经核验的外部内容不能作为已确认事实输出。',
|
|
162
|
+
'',
|
|
163
|
+
'## 触发信号',
|
|
164
|
+
'',
|
|
165
|
+
'- 用户提到 OpenPrd、OpenSpec、Superpowers、Anthropic Skills、Lark CLI、Agent harness、AI code review、PR review、review lane、long-running agents、context engineering、prompt engineering、最佳实践、对标、参考、复刻或优化设计。',
|
|
166
|
+
'- 用户提到图标、icon、图标站、图标库、图标资源、UI 图标、AI 图标、技术图标、3D 图标、功能图标、iconfont 或视觉资产参考。',
|
|
167
|
+
'- 用户要求解释某个 Codex / Claude / Cursor agent 为什么没有发现 skill,或希望提升 skill 自动识别、路由、生成、安装和持续执行能力。',
|
|
168
|
+
'- 用户没有显式说 skill 名也要触发;不要要求用户记住 `$openprd-benchmark-router`。',
|
|
169
|
+
'',
|
|
170
|
+
'## 路由流程',
|
|
171
|
+
'',
|
|
172
|
+
'1. 先识别优化对象:OpenPrd 产品/PRD 流程、CLI、skill 体系、长程任务、通用 harness、AI code review / PR review harness、context engineering、prompt engineering、图标资源或图标实现库。',
|
|
173
|
+
'2. 读取当前工作区证据:`.openprd/`、`.openprd/benchmarks/index.md`、`.openprd/benchmarks/sources.yaml`、`AGENTS.md`、repo-local skills、生成的 `.codex/.claude/.cursor` 引导和相关源码。',
|
|
174
|
+
'3. 选择最小足够的外部证据源:公开 GitHub 仓库走 DeepWiki;第三方工具、SDK、CLI 或官方 API 用 Context7;产品官方文档、工程博客和一手资料用官方来源。',
|
|
175
|
+
'4. 形成 OpenPrd 设计判断时,明确区分已证实事实、从来源归纳出的设计原则,以及对本项目的推断。',
|
|
176
|
+
'5. 用分析维度提炼可迁移原则,避免照搬表面功能。',
|
|
177
|
+
'6. 如果任务变成大量参考项目行为挖掘、长时间覆盖或需求补全,再路由到 `$openprd-discovery-loop` 承接持续调研。',
|
|
178
|
+
'',
|
|
179
|
+
'## Project Registry',
|
|
180
|
+
'',
|
|
181
|
+
'- 项目自己的 `.openprd/benchmarks/` 优先于 OpenPrd 内置 Source Map。',
|
|
182
|
+
'- `sources.yaml` 里的 approved source 是长期可复用参考;`inbox/` 里的 candidate 只表示待确认线索。',
|
|
183
|
+
'- 用 `openprd benchmark add <url|repo|file>` 写入 candidate,用 `openprd benchmark approve <id>` 纳入 approved registry。',
|
|
184
|
+
'- 用 `openprd benchmark verify` 检查重复来源、失效链接、缺失本地文件和过宽触发规则。',
|
|
185
|
+
'',
|
|
186
|
+
'## Source Policy',
|
|
187
|
+
'',
|
|
188
|
+
'- GitHub 仓库:需要理解架构、核心模块、关键流程或对标结论时,先用 DeepWiki。默认顺序是 `read_wiki_structure` 1 次,再 `ask_question` 1-2 次;只有在本地源码和已有结论仍不足时才追加。DeepWiki 不可用或覆盖不足时,再回退到 GitHub README、源码和官方文档。',
|
|
189
|
+
'- 官方技术文档:涉及第三方库、框架、API、SDK、MCP、CLI 工具的用法、配置、限制、版本差异或迁移路径时,先检查本地代码、锁文件、README、类型定义;本地不足时再用 Context7,默认顺序是 `resolve_library_id` 1 次,再 `query_docs` 1-2 次。Context7 不足时说明缺口,再补官方文档、源码或其他一手资料。',
|
|
190
|
+
'- 工程文章和产品文档:优先读取当前线上一手页面,只抽取和当前任务相关的观点与设计原则,不复制长文;如果内容可能过时,要说明时效风险。',
|
|
191
|
+
'- 本地源码优先:当前工作区已经有相关源码时,常规修 bug、查实现、改功能优先读本地代码;DeepWiki 主要用于外部仓库架构理解和对标分析。',
|
|
192
|
+
'- 停止调研:找到足以支持当前决策的 1-3 个高相关来源后停止扩展;候选来源重复时保留更权威、更新或更贴近当前任务的来源。',
|
|
193
|
+
'- 追加调用前先写清“已确认什么、还缺什么”;不要为了同一问题只换个说法反复查询。',
|
|
194
|
+
'',
|
|
195
|
+
'## Source Map',
|
|
196
|
+
'',
|
|
197
|
+
'- OpenPrd / PRD 设计对标:`obra/superpowers`、`Fission-AI/OpenSpec`。',
|
|
198
|
+
'- CLI 与 skill 体系对标:`larksuite/cli`、`anthropics/skills`、Claude Skills 官方文档、Claude Code Skills 官方文档。',
|
|
199
|
+
'- 长程 Agent 任务:Anthropic long-running agents harness 工程文章。',
|
|
200
|
+
'- 通用 harness:OpenAI harness engineering、LangChain agent harness anatomy。',
|
|
201
|
+
'- AI code review / PR review harness:Nolan Lawson 的 “Using AI to write better code more slowly”、Milvus 关于多模型代码审查辩论/交叉验证的实验文章。',
|
|
202
|
+
'- Context engineering:Manus context engineering、Anthropic context engineering。',
|
|
203
|
+
'- Prompt engineering:OpenAI prompt engineering / prompt guidance、Claude prompt engineering、Gemini prompting strategies。',
|
|
204
|
+
'- 图标资源站一级最佳实践:UI 图标优先看 Phosphor Icons(https://phosphoricons.com/);AI 公司与产品图标看 LobeHub Icons(https://lobehub.com/icons);技术栈图标看 Tech Icons(https://techicons.dev/);透明底 3D 图标看 Thiings(https://www.thiings.co/things);功能图标、矢量插画、3D 插画和字体资源看 iconfont(https://www.iconfont.cn/)。',
|
|
205
|
+
'- 图标实现库二级最佳实践:Lucide、Tabler、React Icons。需要落到前端代码时,再结合当前项目框架、包管理器、bundle 体积和导入方式选择具体库。',
|
|
206
|
+
'',
|
|
207
|
+
'## Evaluation Lenses',
|
|
208
|
+
'',
|
|
209
|
+
'- 产品与工作流:用户从哪里开始、如何知道下一步、模糊输入如何变成结构化产物、哪些步骤要保存/展示/恢复、哪些步骤必须保留用户确认。',
|
|
210
|
+
'- Agent 与 Harness:目标、边界、停止条件、工具选择、进度记录、证据、验证结果、失败恢复和人工接管点是否清楚。',
|
|
211
|
+
'- PR 审查 lane:reviewer 是否独立审查、主代理是否先汇总再验证、是否有误报过滤、agreement matrix、严重级别和 merge recommendation。',
|
|
212
|
+
'- 上下文工程:哪些信息常驻、哪些按需检索,是否使用稳定路径、链接和来源 ID 支持 just-in-time 检索,如何处理过期、冲突和可信度。',
|
|
213
|
+
'- 提示词与 Skill 设计:触发描述是否具体但不过度强制,主说明是否短,细节是否按需放到 reference,是否明确不要硬套参考源。',
|
|
214
|
+
'- 图标与视觉资产:先判断用途是 UI、AI 品牌、技术栈、3D 物件还是功能图标;优先选最贴近用途的资源站,再在实现阶段选择合适的代码图标库。',
|
|
215
|
+
'- CLI 与开发者体验:命令是否可发现、可组合、可预测;错误信息是否说明发生了什么、影响是什么、下一步怎么做;危险操作是否有确认。',
|
|
216
|
+
'',
|
|
217
|
+
'## 设计输出',
|
|
218
|
+
'',
|
|
219
|
+
'- 给出 OpenPrd 应该内置什么、生成什么、路由什么、保留什么门禁。',
|
|
220
|
+
'- 优先把结论落到 `CANONICAL_SKILLS`、repo-local skills、AGENTS/CLAUDE/Cursor 生成规则、hooks 或测试,而不是停留在口头建议。',
|
|
221
|
+
'- 不把外部项目整包复制进 OpenPrd;只吸收可验证的路由、生成、门禁、状态承接和用户体验原则。',
|
|
222
|
+
'- 需要显式说明时,简短写出参考了哪个来源、借鉴点、适用原因、不照搬边界,以及落到当前任务的具体决策。',
|
|
223
|
+
'',
|
|
224
|
+
'## Stop Rule',
|
|
225
|
+
'',
|
|
226
|
+
'- 同一来源默认只做一次结构理解和一到两次聚焦问题;证据足够支撑当前决策后立即停止调研。',
|
|
227
|
+
'- 如果 DeepWiki、Context7 或官方资料覆盖不足,明确说明缺口,再把后续结论标为推断。',
|
|
228
|
+
'',
|
|
229
|
+
].join('\n'),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'openprd-harness',
|
|
233
|
+
description: '驱动 OpenPrd 工作区完成 clarify、synthesize、diagram、freeze、handoff、change、tasks 和验证。',
|
|
234
|
+
body: [
|
|
235
|
+
'# OpenPrd Harness',
|
|
236
|
+
'',
|
|
237
|
+
'当用户要求产品规划、需求细化、实现准备或执行就绪时,使用这份 skill。',
|
|
238
|
+
'',
|
|
239
|
+
'## 默认流程',
|
|
240
|
+
'',
|
|
241
|
+
'1. 先运行 `openprd run . --context`,获取 hook-stable 执行视图。',
|
|
242
|
+
'2. 先判断当前用户意图,再决定是否跟随建议。',
|
|
243
|
+
'- 会话 ID 续接:用户给出会话 ID 并要求继续时,把它当成工具无关的历史会话续接请求;先精确恢复该会话历史,不要把当前 active change、相似历史或当前 requirement gate 当成替代目标,也不要把它称为工具专属 ID。',
|
|
244
|
+
'3. 面对规划、分析、架构评审、“怎么改”或“会动哪些文件”类请求,保持只读并基于代码、文档和状态回答。',
|
|
245
|
+
'4. 需要完整工作流细节时,运行 `openprd status .` 和 `openprd next .`。',
|
|
246
|
+
'5. 涉及最佳实践、benchmark、对标、参考产品、prompt engineering、Agent harness、context engineering、图标资源、CLI 或 skill 体系设计时,先使用 `$openprd-benchmark-router`。',
|
|
247
|
+
'6. 先用 `$openprd-requirement-intake` 做 L0/L1/L2 语义分流:L0 小修直接处理并事后说明;L1 给对话内 mini-plan 后执行;L2 高影响或边界不清的需求在改代码前必须先走需求入口:`openprd clarify .` 会生成需求入口自省,并只在对话内输出澄清摘要或简短清单;正式 HTML 评审留给后续 review。',
|
|
248
|
+
'7. 事实缺失时,用 `openprd clarify .` 和 `openprd capture .` 补全,再 synthesize/review、生成或检查 change、拆任务。`clarifyPresentation.mode` 为 `inline` 或 `inline-with-checklist`,直接在对话中用目标、范围、非目标和验收方式压缩确认,不打开澄清 HTML。review 重点摘要胶囊应控制在 15 个字以内,作为扫读标签,不写成长句;对用户给稳定 artifact 路径,确认命令使用页面复制出的 `--version`、`--digest` 和 `--work-unit`,不要把可被其他对话覆盖的 active review 当成唯一确认入口,也不要把“可以开做”“继续实现”之类实现授权当成 `review --mark confirmed` 的依据。如果 synthesize 被简体中文 spec 预检阻断,先把纯内部措辞整理用 `openprd capture . --source agent-normalized` 写回,再重新 synthesize;这类非语义规范化不应重开用户 review。默认 approval policy 是 decision-points:需要时保留稳定 `review.html`,但只有当前 lane 仍要求人类决策时才停下来请求确认;如果用户一开始就明确要求直接做且不需要再评审/确认,则允许按当前稳定 artifact 的精确 `version + digest + work-unit` 记录 review,再继续 change/tasks。若用户原始意图已明确要求实现,则在当前 approval policy 满足且 tasks 就绪后直接进入执行;否则等待一句明确的执行指令。',
|
|
249
|
+
'8. 评审页里的需求关系图、需求流程图和重点摘要不要靠 HTML 截断;`openprd synthesize` 生成版本快照后,不要直接让用户确认 review。必须先用 `openprd review-presentation . --template` 查看展示文案契约,让 Agent 按 reviewPresentation 写短文案,再用 `openprd review-presentation . --presentation <json> --write --fail-on-violation` 校验并写回;脚本会在通过后写入校验元信息并重渲染可确认 review.html。超限时按脚本返回的 jsonPath 和字数限制重新提炼,不手工改快照、不裁剪原文。',
|
|
250
|
+
'9. 对外说明默认用业务和产品语言,先给结论和下一步;涉及第三方 API、模型、云服务或付费工具时,用表格比较多家方案的效果、价格、接入成本、限制、风险和推荐理由,默认选择性价比最优;当用户的问题包含多个对象、方案、文件、场景、风险、验证项、素材或任务,并且需要同时呈现状态、证据、影响、动作或推荐时,主动使用 Markdown 表格,单一结论、代码示例、命令示例和叙事型说明不要强行表格化。',
|
|
251
|
+
'10. 当 PRD 需要进入实现准备时,再运行 `openprd change . --generate --change <id>`。',
|
|
252
|
+
'11. 长程实现使用 `openprd loop . --plan --change <id>`,并且只有用户明确要求开发、继续任务、深度调研、对标复刻或 commit 时才执行单任务 fresh session。',
|
|
253
|
+
'12. 代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 `openprd dev-check . <file...>` 或 `node scripts/openprd-dev-check.mjs . <file...>` 回顾行数状态:700 行以内正常,701-1500 行需说明局部职责,超过 1500 行要判断本轮是否扩大职责,扩大则先重构/拆分/解耦并复查,窄 bugfix 或小修暂不拆时说明原因和后续拆分建议。',
|
|
254
|
+
'13. 如果执行中发现新代码后缀、豁免路径、命令别名、项目约定或用户偏好,不要中途打断任务。工具识别能力补全和减少重复打扰的高置信低风险项可自动应用并记录;用户偏好、项目协作规矩和 OpenPrd 默认行为形成 growth candidate,收工时用 `openprd grow . --review` 集中确认。',
|
|
255
|
+
'14. 维护 OpenPrd 本身时,只要新增或修改配置类能力(阈值、规则、识别、豁免、命令别名、环境差异、用户偏好或策略开关),默认先做 grow-aware 自检:高置信应可成长时直接纳入 `openprd grow` 体系;不确定时主动询问用户是否做成可成长配置。',
|
|
256
|
+
'15. 实现过程中,每次新增或修改文件都做文档影响检查,补齐缺失的 `docs/basic/`、文件说明书和文件夹 README,并更新受影响文档;涉及后端、脚本、Agent、工具链、服务或数据处理变更时,把 CLI 与 API 视为同级接入面:同步检查命令入口、参数、输出契约、`help`、`doctor`、`dry-run`、`status` 与接口协议、返回结构、身份边界是否受影响,并更新 `docs/basic/backend-structure.md` 或明确写不适用原因。',
|
|
257
|
+
'16. 用户要求生成图片、封面图、配图、海报、插画、图标、贴纸、头像、banner、主视觉/KV、运营图、效果图、视觉稿、mockup、先看样子或先确认设计方向时,默认直接调用 Codex 原生 Image 2 生图能力产出图片;对 logo、icon、avatar、badge 等开发素材,如果用户未明确要求 mockup、场景图、设备框、卡片承载、名片/包装展示或参考界面复刻,默认按独立素材输出(standalone asset)处理:使用全画布单主体,不额外添加 UI frame、卡片、设备壳、名片、桌面陈列、手持实拍或其他展示容器。只有当用户明确要求 mockup、场景化效果图、容器化呈现,或参考图本身包含这些结构时,才生成对应容器或场景;除非用户明确指定 HTML、SVG、CSS、Canvas、代码稿或可编辑矢量/source artifact,不要改用临时 HTML/SVG/CSS 再截图。OpenPrd 的 `review.html` 只用于需求评审,不能替代图片或效果图生成。',
|
|
258
|
+
'17. 界面、页面、视觉、样式或前端体验任务中,如果已经有效果图、设计稿、图片资产或用户给图且进入实现阶段,阶段性完成后先截实现图,再运行 `openprd visual-compare . --reference <效果图> --actual <实现截图>`。默认输出 JPG 到 `.openprd/harness/visual-reviews/`,左侧标注“效果图”、右侧标注“实现截图”;查看合成图后继续复刻,直到没有明显视觉差异。',
|
|
259
|
+
'18. 声称就绪前,运行 `openprd standards . --verify` 和 `openprd run . --verify`。',
|
|
260
|
+
'19. 阶段性代码完成后,运行 `openprd quality . --verify`,把 HTML 质量评估报告当作当前场景必需 EVO 门禁、日志、业务成本与滥用护栏、冒烟覆盖、性能、极端场景和项目知识的评审产物。',
|
|
261
|
+
'20. `AGENTS.md` 只保留轻量合同;入口路由看 `$openprd-router`,具体命令速查看 `.openprd/harness/command-catalog.md`,更细的工作流步骤、路由边界和 hook 门禁以这份 skill、`$openprd-shared` 和 `$openprd-benchmark-router` 为准。',
|
|
262
|
+
'21. hook 会强制阻断几类场景:需求入口未完成就写实现、外部证据不足就直接改第三方集成、skill/AGENTS 变更未先可视化确认、以及敏感信息场景下直接读原始 vault 文件。',
|
|
263
|
+
'',
|
|
264
|
+
'## 门禁协议',
|
|
265
|
+
'',
|
|
266
|
+
'- 不要跳过 `openprd run . --context`;它是最适合 hooks 的控制面。',
|
|
267
|
+
'- 不要把 `run --context` 里的建议当成直接用户命令。',
|
|
268
|
+
'- 面对“看看、规划、梳理、分析、评估、怎么改、预计动哪些文件、review、explain”等只读意图,不运行 OpenPrd 写入命令。',
|
|
269
|
+
'- 现有项目需求仍模糊时,优先 discovery,再考虑 synthesize。',
|
|
270
|
+
'- 进入定稿或交接前,运行 `openprd run . --verify` 并确认 review blocker 已关闭。',
|
|
271
|
+
'- 声称实现就绪前,审阅最新 `.openprd/quality/reports/*.html` HTML 质量评估报告;`productionReady=false` 时不得宣称就绪。',
|
|
272
|
+
'- accepted spec 推进前,先运行 `openprd change . --validate --change <id>`。',
|
|
273
|
+
'',
|
|
274
|
+
'## hook 驱动循环',
|
|
275
|
+
'',
|
|
276
|
+
'- 把 `.openprd/harness/run-state.json` 和 `iterations.jsonl` 当成持久循环状态。',
|
|
277
|
+
'- 默认 lite hooks 不记录每一轮工具细节,但会在明确 OpenPrd / 深度工作提示词和产品、模块、流程需求下注入上下文;复杂或模糊需求提示先做三轮 Requirement Intake Reflection,轻量写入门禁会阻断过早改代码;本轮准备结束时再通过 `Stop` 做一次轻量项目经验回顾。',
|
|
278
|
+
'- 只有项目确实需要完整遥测时才使用 `--hook-profile full`。',
|
|
279
|
+
'- 上下文注入后,hooks 会从 OpenPrd 状态里推荐下一项 task、discovery 或 workflow 动作。',
|
|
280
|
+
'- 门禁失败时,任务或覆盖项保持未完成状态,让下一轮继续重试。',
|
|
281
|
+
'- 可以把跨任务可复用经验记录到 `.openprd/harness/learnings.md`、本地 `AGENTS.md` 或 `docs/basic/`。',
|
|
282
|
+
'',
|
|
283
|
+
'## 长程实现循环',
|
|
284
|
+
'',
|
|
285
|
+
'- 运行 `openprd loop . --init`,再运行 `openprd loop . --plan --change <id>` 生成 `.openprd/harness/feature-list.json`。',
|
|
286
|
+
'- 用 `openprd loop . --next` 找到下一个依赖已满足的任务。',
|
|
287
|
+
'- 用 `openprd loop . --run --agent codex --dry-run` 或 `openprd loop . --run --agent claude --dry-run` 生成单任务 prompt 和启动命令。',
|
|
288
|
+
'- 只有当前用户消息明确要求执行开发、继续任务或深度调研时,才运行 `openprd loop . --run`。单纯的规划问题不构成执行授权。',
|
|
289
|
+
'- 每个 loop 任务对应一个全新 agent 会话边界,不要在同一会话里继续下一项任务。',
|
|
290
|
+
'- 只有在任务 verify 命令和 `openprd run . --verify` 通过后,且用户明确要求 commit 时,才用 `openprd loop . --finish --item <task-id> --commit` 收尾。',
|
|
291
|
+
'- 前端界面任务里,Codex desktop 优先用 Computer Use;Codex CLI 和 Claude Code 优先用 Playwright、MCP 浏览器自动化或项目现有 e2e 工具。',
|
|
292
|
+
'- 用户只是要求生成图片、封面图、配图、海报、插画、图标、贴纸、头像、banner、主视觉/KV、运营图、效果图、视觉稿、mockup 或先看样子时,默认调用 Codex 原生 Image 2 生成图片;对 logo、icon、avatar、badge 等开发素材,如果用户未明确要求 mockup、场景图、设备框、卡片承载、名片/包装展示或参考界面复刻,默认按独立素材输出(standalone asset)处理:使用全画布单主体,不额外添加 UI frame、卡片、设备壳、名片、桌面陈列、手持实拍或其他展示容器。只有当用户明确要求 mockup、场景化效果图、容器化呈现,或参考图本身包含这些结构时,才生成对应容器或场景;除非用户明确指定 HTML/SVG/CSS/Canvas/代码稿,不要生成临时 HTML 再截图。',
|
|
293
|
+
'- 如果已有参考效果图、图片资产或用户给图并进入实现阶段,阶段性完成后必须生成实现截图,并用 `openprd visual-compare . --reference <效果图> --actual <实现截图>` 输出 JPG 视觉对比图。未查看对比图、或对比图仍有明显差异时,不要声称界面复刻完成。',
|
|
294
|
+
'- `openprd loop . --finish` 会写入 `.openprd/harness/test-reports/<task-id>.md`;把这份结构化测试报告和任务改动一起提交。',
|
|
295
|
+
'- 让 `.openprd/harness/feature-list.json`、`progress.md`、`agent-sessions.jsonl`、`loop-state.json`、`loop-prompts/` 和 `test-reports/` 成为持久状态。',
|
|
296
|
+
'',
|
|
297
|
+
'## 失败处理',
|
|
298
|
+
'',
|
|
299
|
+
'- 命令失败后不要凭直觉继续。',
|
|
300
|
+
'- 重新运行 `openprd run . --context`、`openprd doctor .`,并按输出里的修复命令处理。',
|
|
301
|
+
'- 如果失败假设影响产品范围,把它保留在 `.openprd/engagements/active/open-questions.md`。',
|
|
302
|
+
'',
|
|
303
|
+
'## 历史项目',
|
|
304
|
+
'',
|
|
305
|
+
'- 批量处理旧项目之前,先用 `openprd fleet <root> --dry-run` 审计。',
|
|
306
|
+
'- 已有历史项目要先回填全局名册时,用 `openprd fleet <root> --sync-registry` 把已初始化的 `.openprd/` 工作区写回 `~/.openprd/registry/workspaces.jsonl`。',
|
|
307
|
+
'- 用 `openprd fleet <root> --backfill-work-units` 为已有 PRD 版本补 work unit、digest 和稳定评审页。',
|
|
308
|
+
'- 用 `openprd fleet <root> --update-openprd` 只刷新已经包含 `.openprd/` 的项目,并顺带补齐历史 work unit 绑定。',
|
|
309
|
+
'- 除非用户明确要求 OpenPrd 接管 agent-only 或 plain 项目,否则不要使用 `--setup-missing`。',
|
|
310
|
+
'',
|
|
311
|
+
].join('\n'),
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: 'openprd-standards',
|
|
315
|
+
description: '初始化并校验 `docs/basic`、文件说明书和文件夹 README 标准。',
|
|
316
|
+
body: [
|
|
317
|
+
'# OpenPrd Standards',
|
|
318
|
+
'',
|
|
319
|
+
'当文档、文件说明书、文件夹 README 或实现就绪检查在范围内时,使用这份 skill。',
|
|
320
|
+
'',
|
|
321
|
+
'## 必需文档',
|
|
322
|
+
'',
|
|
323
|
+
'- `docs/basic/file-structure.md`',
|
|
324
|
+
'- `docs/basic/app-flow.md`',
|
|
325
|
+
'- `docs/basic/prd.md`',
|
|
326
|
+
'- `docs/basic/frontend-guidelines.md`',
|
|
327
|
+
'- `docs/basic/backend-structure.md`',
|
|
328
|
+
'- `docs/basic/tech-stack.md`',
|
|
329
|
+
'',
|
|
330
|
+
'报告实现就绪前,先运行 `openprd standards . --verify`。',
|
|
331
|
+
'对包含源码文件的项目,这个门禁还要求 `docs/basic/` 内容具体可用、文件头说明书存在,以及 `[project]_[folder]_README.md` 文件夹说明完整;如果涉及后端实现,`docs/basic/backend-structure.md` 还必须显式覆盖 CLI 接入面和 API 接入面,或写明不适用原因。',
|
|
332
|
+
'研发期代码修改完成后、最终回复前,运行 `openprd dev-check . <file...>` 或 `node scripts/openprd-dev-check.mjs . <file...>`;该标准层只检查本轮实际 touched code files 的行数状态,不替代 `standards --verify`。',
|
|
333
|
+
'当 dev-check 高置信识别出新的代码扩展名时,可自动补齐识别规则并记录;豁免路径、项目规矩、用户偏好或 OpenPrd 默认行为只作为候选留到收工复盘,用 `openprd grow . --review` 集中确认。',
|
|
334
|
+
'维护 OpenPrd 本身时,新增或修改任何配置类能力都要检查是否应该成为 grow-aware 配置:高置信可复用、可被用户习惯影响、会随项目环境变化的配置默认纳入 `openprd grow`;不确定时主动询问用户;一次性固定规则才保留为静态配置。',
|
|
335
|
+
'',
|
|
336
|
+
'## 文档影响检查',
|
|
337
|
+
'',
|
|
338
|
+
'- 编辑前先识别本次会变化的文件、文件夹、用户流程、架构边界、依赖和产品行为。',
|
|
339
|
+
'- 代码修改完成后用 dev-check 回顾行数:`ok` 可正常收尾,`attention` 需要说明局部职责,`warning` 需要判断本轮是否扩大职责;扩大则先拆分/解耦并复查,窄修暂不拆时说明原因和后续拆分建议。',
|
|
340
|
+
'- 新增配置类能力时同步评审 grow-aware 入口:候选类型、scope、review/apply 行为、拒绝后不重复提示,以及 user-local 与项目共享配置的边界。',
|
|
341
|
+
'- 新增源码文件:如果缺少文件说明书就补上,并确认所在文件夹 README 已存在。',
|
|
342
|
+
'- 修改源码文件:若已有文件说明书,先读取;当文件职责、输入、输出、依赖或维护规则变化时更新它。',
|
|
343
|
+
'- 文件夹内容新增、移动、删除或改作他用:新增或更新文件夹 README,使其反映当前职责和文件布局。',
|
|
344
|
+
'- 功能、流程、架构、依赖或产品行为变化:即使文件已存在,也更新对应的 `docs/basic/` 文档。',
|
|
345
|
+
'- 后端、脚本、Agent、工具链、服务或数据处理变化:把 CLI 与 API 视为同级接入面,更新 `docs/basic/backend-structure.md` 中的命令入口、输出契约、`help`/`doctor`/`dry-run`/`status`、接口协议和不适用说明。',
|
|
346
|
+
'- 若必需文档或说明书缺失,或仍停留在模板态,就绪前必须补齐。',
|
|
347
|
+
'- 如果最终不需要改文档,也要说明文档影响检查已完成,以及为什么可以保持不变。',
|
|
348
|
+
'',
|
|
349
|
+
'## 同步触发条件',
|
|
350
|
+
'',
|
|
351
|
+
'- 文件或文件夹新增、移动、删除:更新 `docs/basic/file-structure.md` 和相关文件夹 README。',
|
|
352
|
+
'- 产品流程、状态、路由或任务行为变化:更新 `docs/basic/app-flow.md`。',
|
|
353
|
+
'- 用户可见能力或验收标准变化:更新 `docs/basic/prd.md`。',
|
|
354
|
+
'- 框架、依赖、运行时或构建命令变化:更新 `docs/basic/tech-stack.md`。',
|
|
355
|
+
'- 前端或后端结构变化:更新对应的 `docs/basic/` 指南;后端变化时同时评估 CLI 与 API 两个接入面。',
|
|
356
|
+
'',
|
|
357
|
+
'## 门禁',
|
|
358
|
+
'',
|
|
359
|
+
'`openprd standards . --verify` 必须在 freeze、handoff、accepted spec apply/archive、commit、push、release 和 publish 之前通过。',
|
|
360
|
+
'',
|
|
361
|
+
].join('\n'),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: 'openprd-discovery-loop',
|
|
365
|
+
description: '面向现有项目、参考项目和模糊需求的持续 OpenPrd discovery。',
|
|
366
|
+
body: [
|
|
367
|
+
'# OpenPrd Discovery Loop',
|
|
368
|
+
'',
|
|
369
|
+
'当用户要求继续、深挖、补全、对比、复刻、全面梳理 requirements,或进行大量只读扫描时,使用这份 skill。',
|
|
370
|
+
'',
|
|
371
|
+
'## 大量只读扫描调度',
|
|
372
|
+
'',
|
|
373
|
+
'- 日常任务仍由主 agent 先直接读取本地上下文;不要因为用户只说“看看、分析、梳理、定位、排查”就自动并行。',
|
|
374
|
+
'- 用户明确要求深度分析、深入调研、全面梳理、多角度评估、交叉验证、并行排查、对标复刻或风险审查时,优先考虑只读 subagent。',
|
|
375
|
+
'- 任务需要同时阅读多个目录、文档、模块、日志、历史实现或参考项目,且并行收集证据能明显减少主上下文污染或节省时间时,可以启动。',
|
|
376
|
+
'- 任务涉及外部技术事实、公开仓库对标、复杂排障、发布风险或安全风险,且需要独立复核时,可以启动;仍必须遵守 Context7、DeepWiki、secrets-vault 和长文件门禁。',
|
|
377
|
+
'- 用户明确说“不用 subagent / 直接做 / 先别并行 / 只回答”时,不启动。',
|
|
378
|
+
'- 单文件小改、明确文案微调、简单命令、非常短的问题或清晰 bug 修复,默认不启动。',
|
|
379
|
+
'- 一旦进入深度研究型 subagent 流程,默认使用 3 个只读 subagent:2 个独立调研执行者 + 1 个审查/交叉验证者。',
|
|
380
|
+
'- 最多启动 5 个 subagent:最多 4 个调研执行者 + 1 个审查者。只有任务天然拆成 4 个互不冲突的研究分支时才扩到 5 个。',
|
|
381
|
+
'- 代码与文档调研优先使用 `spark-code-researcher`、`spark-doc-reader` 或 `documentation-explore`;对标复刻用 `electron-parity-mapper`;安装发布或渠道排障用 `release-diagnostics-researcher`、`channel-debug-researcher`;审查与风险扫描用 `skill-workflow-reviewer`、`security-risk-researcher`。',
|
|
382
|
+
'- 每个 subagent 只回答一个清晰问题,不再继续 spawn;主 agent 负责决策、整合和所有写入,subagent 只做只读调研、归纳和交叉验证。',
|
|
383
|
+
'- subagent 输出必须回到主 agent 汇总;写入 discovery claim、requirements、specs 或 tasks 前,主 agent 必须把结论映射到证据路径、置信度和未解决问题。',
|
|
384
|
+
'',
|
|
385
|
+
'## 循环',
|
|
386
|
+
'',
|
|
387
|
+
'- 用 `openprd discovery . --mode <brownfield|reference|requirement>` 启动或恢复。',
|
|
388
|
+
'- 每次只推进一个有证据支撑的覆盖项。',
|
|
389
|
+
'- 报告运行健康前,用 `openprd discovery . --verify` 做校验。',
|
|
390
|
+
'- 通过 `openprd standards . --verify` 保持基线文档标准同步。',
|
|
391
|
+
'- 阶段性实现或任务完成后,用 `openprd quality . --verify` 审查 HTML 质量评估报告里的场景标签、必需 EVO 门禁、日志、业务护栏、冒烟覆盖、性能和知识缺口。',
|
|
392
|
+
'',
|
|
393
|
+
'## 深度规则',
|
|
394
|
+
'',
|
|
395
|
+
'- 每个 claim 都要带来源、证据路径和置信度。',
|
|
396
|
+
'- 推断出的行为不能直接变成 accepted requirement,必须保持可评审。',
|
|
397
|
+
'- 大型任务文件必须分片并通过校验。',
|
|
398
|
+
'- 只有在覆盖耗尽、被阻塞,或明确交接后才停止。',
|
|
399
|
+
'',
|
|
400
|
+
].join('\n'),
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
id: 'openprd-learning-review',
|
|
404
|
+
description: '为 OpenPrd 工作区生成归档学习包、题材模板、证据清单、图文讲解模块和 HTML 电子书阅读器。',
|
|
405
|
+
body: [
|
|
406
|
+
'# OpenPrd Learning Review',
|
|
407
|
+
'',
|
|
408
|
+
'当用户希望生成复盘学习包、题材模板库、证据清单、图文讲解模块、检索模块、工作示例或 OpenPrd 工作区里的 HTML 电子书阅读器时,使用这份 skill。',
|
|
409
|
+
'',
|
|
410
|
+
'## 产出物',
|
|
411
|
+
'',
|
|
412
|
+
'- `learning-content.json`:版本化内容契约',
|
|
413
|
+
'- `evidence-manifest.json`:source id、digest、摘录、claim 和缺口',
|
|
414
|
+
'- `learning-content.md`:书籍式阅读稿',
|
|
415
|
+
'- `reader.html`:固定电子书阅读器界面,支持章节级 `visualExplainer` 图卡',
|
|
416
|
+
'- `assets/`:可选图片素材目录,用于归档 Codex Image 2 生成的图文解释图片',
|
|
417
|
+
'- `learning-package.json` 和 `.openprd/learning/index.json`:归档元数据',
|
|
418
|
+
'',
|
|
419
|
+
'## 工作流程',
|
|
420
|
+
'',
|
|
421
|
+
'1. 从 `.openprd/` 重建状态,并识别触发源是 loop finish 还是手动请求。',
|
|
422
|
+
'2. 从参考库里选择题材。主题没有特殊要求时,默认使用 `internet-product`。',
|
|
423
|
+
'3. 写正文前先从工作区状态、`docs/basic` 和 loop 报告收集证据。',
|
|
424
|
+
'4. 分离证据清单、叙事正文和渲染器;所有判断都必须能引用 source id。面向产品或非技术读者时,优先补 `visualExplainer` 图卡。',
|
|
425
|
+
'5. 尽可能在每章加入检索模块、工作示例模块和必要的图文比喻卡。',
|
|
426
|
+
'6. 把学习包归档到 `.openprd/learning/archive/<packageId>/`,并在合适时打开 `reader.html`。',
|
|
427
|
+
'',
|
|
428
|
+
'## 扩展规则',
|
|
429
|
+
'',
|
|
430
|
+
'- 新增题材时扩展参考库,不要分叉渲染器。',
|
|
431
|
+
'- 契约必须版本化;`openprd.learning-content.v1` 的演进要通过新版本完成。',
|
|
432
|
+
'- 任何无法追溯到来源的句子都要显式标为推断。',
|
|
433
|
+
'- 把阅读器保持为稳定的 HTML 电子书界面,包含 TOC、进度、上一章/下一章控制、章节内轻量证据锚点和可选图文解释卡。',
|
|
434
|
+
'',
|
|
435
|
+
'## 参考资料',
|
|
436
|
+
'',
|
|
437
|
+
'- `skills/openprd-learning-review/references/genre-library.md`',
|
|
438
|
+
'- `skills/openprd-learning-review/references/content-contract.md`',
|
|
439
|
+
'- `skills/openprd-learning-review/references/evidence-manifest.md`',
|
|
440
|
+
'- `skills/openprd-learning-review/references/ebook-reader.md`',
|
|
441
|
+
'- `skills/openprd-learning-review/references/retrieval-worked-example.md`',
|
|
442
|
+
'- `skills/openprd-learning-review/references/quality-rubric.md`',
|
|
443
|
+
'',
|
|
444
|
+
].join('\n'),
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
id: 'openprd-quality',
|
|
448
|
+
description: '评估可观测性、业务成本与滥用护栏、评估执行环境覆盖、性能基线、极端场景,以及 HTML 质量评估报告和项目知识 Skill。',
|
|
449
|
+
body: [
|
|
450
|
+
'# OpenPrd Quality',
|
|
451
|
+
'',
|
|
452
|
+
'当实现就绪、日志、链路追踪、免费额度、业务成本、滥用防护、评估执行环境、冒烟测试、性能基线、压力数据或项目级经验 Skill 在范围内时,使用这份 skill。',
|
|
453
|
+
'',
|
|
454
|
+
'## 命令',
|
|
455
|
+
'',
|
|
456
|
+
'- `openprd quality . --init`:初始化 `.openprd/quality/config.json` 和 `.openprd/knowledge/`',
|
|
457
|
+
'- `openprd quality . --verify`:在 `.openprd/quality/reports/` 下生成 JSON 和 HTML 质量评估报告',
|
|
458
|
+
'- `openprd quality . --learn --from <report-id-or-json>`:把已修复或已审查的质量问题沉淀为项目级经验 Skill',
|
|
459
|
+
'- `openprd grow . --review`:审查执行中发现的可复用配置、规则候选或 user-local 偏好;和 `quality --learn` 互补,前者沉淀操作配置,后者沉淀已验证质量经验。',
|
|
460
|
+
'',
|
|
461
|
+
'## 审查契约',
|
|
462
|
+
'',
|
|
463
|
+
'- 场景画像:先判断当前变更是基础、前端、桌面端、后端、成本、安全、性能、极端数据还是发布交付场景,再确定必需 EVO 门禁。',
|
|
464
|
+
'- 可观测性:确认中心化 logs / traces / errors、共享 trace/request/task/error id、脱敏、保留期和查询示例。',
|
|
465
|
+
'- 业务护栏:涉及免费用户、额度、AI 调用、第三方 API、生成、存储或下载时,确认成本来源、用户级限制、负向验证、监控、报警和止损动作。',
|
|
466
|
+
'- 评估执行环境:确认冒烟测试、任务到功能覆盖、正常性能基线和极端数据压力场景;脚本存在只代表能力,不能替代本次运行证据。',
|
|
467
|
+
'- 视觉评审证据:涉及界面视觉实现且已有参考效果图时,确认 `.openprd/harness/visual-reviews/` 下存在本次 `openprd visual-compare` 输出的 JPG,并且 Agent 已基于合成图复核差异。',
|
|
468
|
+
'- HTML 报告:把 `.openprd/quality/reports/*.html` 当成面向人的评审产物,而不是次级导出。',
|
|
469
|
+
'- 知识沉淀:当某个已验证修复具备重复性、高影响、隐藏性或由 agent 误判引发时,把模式抽象到 `.openprd/knowledge/skills/<skill>/SKILL.md`。',
|
|
470
|
+
'- 自我成长:当问题来自配置缺口、文件识别、命令习惯或用户偏好时,优先记录为 `.openprd/growth` 候选,经用户确认后固化;不要把个人偏好混进项目共享质量经验。',
|
|
471
|
+
'',
|
|
472
|
+
'## 就绪规则',
|
|
473
|
+
'',
|
|
474
|
+
'- `openprd run . --verify` 中只要质量报告 `productionReady=false`,就不能宣称整体就绪。',
|
|
475
|
+
'- UI 任务有参考图但缺少 visual-compare 输出时,不要宣称视觉实现完成;如果对比图仍有明显偏差,先返工而不是把差异留给用户发现。',
|
|
476
|
+
'- 最终回复必须列出未通过的必需 EVO 门禁;场景可选门禁可以说明为 advisory,但不能混同为已通过。',
|
|
477
|
+
'',
|
|
478
|
+
'## 收紧规则',
|
|
479
|
+
'',
|
|
480
|
+
'Agent 创建的性能基线从合理的行业平均默认值开始。用户可以放宽或收紧,但 Agent 的自更新只能收紧阈值。',
|
|
481
|
+
'',
|
|
482
|
+
].join('\n'),
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: 'openprd-diagram-review',
|
|
486
|
+
description: '在 freeze 前生成并评审 OpenPrd 架构图和产品流程图。',
|
|
487
|
+
body: [
|
|
488
|
+
'# OpenPrd Diagram Review',
|
|
489
|
+
'',
|
|
490
|
+
'当需要架构、产品流程、用户旅程或可视化确认时,使用这份 skill。',
|
|
491
|
+
'',
|
|
492
|
+
'- 用 `openprd diagram . --type architecture` 生成架构图。',
|
|
493
|
+
'- 用 `openprd diagram . --type product-flow` 生成产品流程图。',
|
|
494
|
+
'- 只有在用户审阅完产物后,才使用 `--mark confirmed`。',
|
|
495
|
+
'',
|
|
496
|
+
'## 契约语言',
|
|
497
|
+
'',
|
|
498
|
+
'- Diagram contract 面向用户。当 `locale` 为 `zh-CN` 时,所有可见文本都要写成简体中文。',
|
|
499
|
+
'- 面向用户的 review.html 或 diagram HTML 文案不要使用 `freeze` 这类内部流程词,改写为“需求定稿前”“进入实现前确认”等业务可理解表达。',
|
|
500
|
+
'- 这包括 `title`、`subtitle`、`components[].name`、`components[].subtitle`、`components[].details`、`flows[].label`、`summaryCards[].title`、`summaryCards[].items`、`sidePanels[].title`、`sidePanels[].items` 和 `reviewInstructions`。',
|
|
501
|
+
'- MotiClaw、Electron、TypeScript、CLI、API、JSON、NDJSON、dry-run、Host API、schema、`waiting_approval` 这类必要术语可以保留,但周围句子必须是简体中文。',
|
|
502
|
+
'- 不要在 zh-CN diagram contract 中保留完整英文句子;运行 `openprd diagram --input` 前先把英文偏重文本改成简体中文。',
|
|
503
|
+
'',
|
|
504
|
+
'## 评审门禁',
|
|
505
|
+
'',
|
|
506
|
+
'- 出图不等于确认。',
|
|
507
|
+
'- 确认必须来自用户或项目 owner 对结构的接受。',
|
|
508
|
+
'- 如果图示影响实现,同步更新 `docs/basic/app-flow.md`、`docs/basic/backend-structure.md` 或 `docs/basic/frontend-guidelines.md`。',
|
|
509
|
+
'',
|
|
510
|
+
].join('\n'),
|
|
511
|
+
},
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
const CANONICAL_COMMANDS = [
|
|
515
|
+
{
|
|
516
|
+
id: 'next',
|
|
517
|
+
title: 'OpenPrd Next',
|
|
518
|
+
body: [
|
|
519
|
+
'Run `openprd status .` and `openprd next .`, summarize the current gate, then execute the suggested OpenPrd action when safe.',
|
|
520
|
+
].join('\n'),
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
id: 'standards',
|
|
524
|
+
title: 'OpenPrd Standards',
|
|
525
|
+
body: [
|
|
526
|
+
'Run `openprd standards . --verify`. If it fails, repair `docs/basic/`, file manual templates, or folder README templates before continuing.',
|
|
527
|
+
].join('\n'),
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
id: 'grow',
|
|
531
|
+
title: 'OpenPrd Grow',
|
|
532
|
+
body: [
|
|
533
|
+
'Treat grow as an end-of-task review layer, not an in-task interruption. Auto-apply high-confidence low-risk tool-recognition fixes such as code-extension detection; queue user preferences, project governance rules, and OpenPrd default behavior as candidates, then run `openprd grow . --review` at wrap-up for user confirmation.',
|
|
534
|
+
].join('\n'),
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
id: 'change',
|
|
538
|
+
title: 'OpenPrd Change',
|
|
539
|
+
body: [
|
|
540
|
+
'Generate or inspect an OpenPrd change. Prefer `openprd change . --generate --change <id>`, then `openprd change . --validate --change <id>`.',
|
|
541
|
+
].join('\n'),
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: 'discovery',
|
|
545
|
+
title: 'OpenPrd Discovery',
|
|
546
|
+
body: [
|
|
547
|
+
'Start or resume OpenPrd discovery. Use `openprd discovery . --resume` when a run exists; otherwise choose the mode from context.',
|
|
548
|
+
].join('\n'),
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
id: 'doctor',
|
|
552
|
+
title: 'OpenPrd Doctor',
|
|
553
|
+
body: [
|
|
554
|
+
'Run `openprd doctor .` and repair missing AGENTS, skills, commands, hooks, standards, or validation gates.',
|
|
555
|
+
].join('\n'),
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: 'verify',
|
|
559
|
+
title: 'OpenPrd Verify',
|
|
560
|
+
body: [
|
|
561
|
+
'Run `openprd run . --verify`. It verifies standards, workspace validation, the currently focused change structure (not just the global active change), and active discovery state, then reports `taskReady` separately from `workspaceReady`.',
|
|
562
|
+
].join('\n'),
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
id: 'visual-compare',
|
|
566
|
+
title: 'OpenPrd Visual Compare',
|
|
567
|
+
body: [
|
|
568
|
+
'When UI work has a reference effect image or user-provided design, capture the implemented UI screenshot, then run `openprd visual-compare . --reference <effect-image> --actual <implementation-screenshot>`.',
|
|
569
|
+
'The command creates a side-by-side JPG under `.openprd/harness/visual-reviews/` by default, with Simplified Chinese labels: left `效果图`, right `实现截图`.',
|
|
570
|
+
'Inspect the generated image and keep iterating until there are no obvious visual differences before claiming completion.',
|
|
571
|
+
].join('\n'),
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: 'run',
|
|
575
|
+
title: 'OpenPrd Run',
|
|
576
|
+
body: [
|
|
577
|
+
'Use the hook-stable OpenPrd execution loop. Start with `openprd run . --context`, execute the recommended task/discovery/workflow action, then run `openprd run . --verify` before claiming completion.',
|
|
578
|
+
'When the user gives a historical session ID, task handle, work unit, or a clear requirement/task description, pass `--message <user-prompt>` so `run --context` resolves that explicit target before considering the global active change. Treat session IDs as tool-neutral; do not require or invent tool-specific ID syntax.',
|
|
579
|
+
'',
|
|
580
|
+
'Intent gate: `openprd run . --context` is advisory. Execute mutating recommendations only when the current user message explicitly asks for development, implementation, task continuation, deep research/benchmarking, replication, or commit. Stay read-only for planning, analysis, review, explanation, and file-impact questions.',
|
|
581
|
+
].join('\n'),
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
id: 'loop',
|
|
585
|
+
title: 'OpenPrd Loop',
|
|
586
|
+
body: [
|
|
587
|
+
'使用长程 agent harness 做开发落地。',
|
|
588
|
+
'',
|
|
589
|
+
'1. Run `openprd loop . --init` once for the workspace.',
|
|
590
|
+
'2. Run `openprd loop . --plan --change <id>` to build the feature list from structured change tasks.',
|
|
591
|
+
'3. Run `openprd loop . --next` to inspect the next dependency-ready task.',
|
|
592
|
+
'4. Run `openprd loop . --run --agent codex --dry-run` or `openprd loop . --run --agent claude --dry-run` to prepare one fresh single-task session.',
|
|
593
|
+
'5. Only run `openprd loop . --run` when the current user message explicitly asks to execute development, continue a task, or perform deep research/benchmarking.',
|
|
594
|
+
'6. 每个任务都必须先自测;前端界面任务在 Codex 桌面优先用 Computer Use,在 CLI/Claude Code 优先用 Playwright 或 MCP 自动化。',
|
|
595
|
+
'7. After the session completes, run `openprd loop . --finish --item <task-id> --commit` only when commit is explicitly part of the requested execution.',
|
|
596
|
+
'',
|
|
597
|
+
'Do not continue into the next task inside the same agent session.',
|
|
598
|
+
].join('\n'),
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
id: 'repair',
|
|
602
|
+
title: 'OpenPrd Repair',
|
|
603
|
+
body: [
|
|
604
|
+
'Use `openprd doctor .` to identify drift or missing generated files. Run `openprd update .` for generated guidance drift, repair standards/docs manually, then re-run verification.',
|
|
605
|
+
].join('\n'),
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: 'onboard',
|
|
609
|
+
title: 'OpenPrd Onboard',
|
|
610
|
+
body: [
|
|
611
|
+
'Explain the current OpenPrd workspace by running `openprd status .`, `openprd next .`, and `openprd doctor .`, then summarize the current gate, blockers, standards state, and safest next command.',
|
|
612
|
+
].join('\n'),
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
id: 'guard',
|
|
616
|
+
title: 'OpenPrd Guard',
|
|
617
|
+
body: [
|
|
618
|
+
'Before a high-risk action, verify the harness gates: `openprd standards . --verify`, `openprd validate .`, and when relevant `openprd change . --validate --change <id>`.',
|
|
619
|
+
].join('\n'),
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: 'fleet',
|
|
623
|
+
title: 'OpenPrd Fleet',
|
|
624
|
+
body: [
|
|
625
|
+
'Audit or update historical projects. Start with `openprd fleet <root> --dry-run`; use `--sync-registry` to backfill the global workspace registry, `--backfill-work-units` for historical PRD identity binding, `--update-openprd` only for projects that already have `.openprd/`, and reserve `--setup-missing` for explicitly selected projects.',
|
|
626
|
+
].join('\n'),
|
|
627
|
+
},
|
|
628
|
+
];
|
|
629
|
+
|
|
630
|
+
function renderCommandCatalog() {
|
|
631
|
+
return [
|
|
632
|
+
'# OpenPrd Command Catalog',
|
|
633
|
+
'',
|
|
634
|
+
'这份清单只负责回答两件事:当前 CLI 有哪些稳定入口,以及什么情况下该用哪条命令。',
|
|
635
|
+
'',
|
|
636
|
+
'## 状态与修复',
|
|
637
|
+
'',
|
|
638
|
+
'- `openprd run . --context`:读取 hook-stable 建议上下文;它是建议,不是自动执行指令。续做历史任务或按用户描述找对应需求/任务时,可带 `--message <用户原话>` 先解析显式目标。',
|
|
639
|
+
'- `openprd run . --verify`:校验当前 run 门禁,并把 `taskReady` 与 `workspaceReady` 分开报告。',
|
|
640
|
+
'- `openprd doctor .`:检查生成引导、hooks、skills、standards 与验证健康度。',
|
|
641
|
+
'- `openprd update .`:修复生成引导、skills、hooks 与 drift。',
|
|
642
|
+
'- `openprd next .`:查看下一步 harness 动作。',
|
|
643
|
+
'',
|
|
644
|
+
'## 需求与评审',
|
|
645
|
+
'',
|
|
646
|
+
'- `openprd clarify .`:生成需求入口自省,并把澄清压缩回对话内确认。',
|
|
647
|
+
'- `openprd capture . --field <path> --value <text|json>`:把用户确认写回状态。',
|
|
648
|
+
'- `openprd synthesize .`:生成可评审 PRD 与 `review.html`。',
|
|
649
|
+
'- `openprd review . --open`:打开当前 PRD review artifact。',
|
|
650
|
+
'- `openprd review . --mark confirmed --version <id> --digest <sha256> --work-unit <id>`:记录当前稳定评审稿;默认用于人类确认后的记录,若当前 lane 已进入 silent-record policy,也只能对精确匹配的稳定 artifact 记录。',
|
|
651
|
+
'',
|
|
652
|
+
'## 设计与实现准备',
|
|
653
|
+
'',
|
|
654
|
+
'- `openprd change . --generate --change <id>`:把 PRD 转成 change。',
|
|
655
|
+
'- `openprd change . --validate --change <id>`:校验 change 结构。',
|
|
656
|
+
'- `openprd tasks . --change <id>`:查看当前 dependency-ready 任务。',
|
|
657
|
+
'- `openprd tasks . --change <id> --advance --verify --item <task-id>`:运行 verify 并推进单个任务。',
|
|
658
|
+
'- `openprd loop . --plan --change <id>`:为长程实现构建单任务列表。',
|
|
659
|
+
'- `openprd loop . --run --agent codex|claude --dry-run`:准备一个 fresh single-task session。',
|
|
660
|
+
'',
|
|
661
|
+
'## Benchmark 与学习包',
|
|
662
|
+
'',
|
|
663
|
+
'- `openprd benchmark add <url|repo|file> --notes <text>`:把外部最佳实践先写入 candidate,用于后续 approve/verify。',
|
|
664
|
+
'- `openprd benchmark list .`:查看当前项目的 approved 与 candidate benchmark source。',
|
|
665
|
+
'- `openprd benchmark approve <benchmark-id>`:把 candidate 纳入项目级长期 registry。',
|
|
666
|
+
'- `openprd benchmark verify .`:检查重复来源、失效链接、缺场景和过宽触发词。',
|
|
667
|
+
'- `openprd learn . --topic <text> --open`:生成当前项目的学习包骨架和 HTML 阅读器。',
|
|
668
|
+
'- `openprd learn . --content-json <file> --open`:让 Agent 写完 `learning-content.json` 后重新渲染最终图文阅读器。',
|
|
669
|
+
'',
|
|
670
|
+
'## 视觉与质量',
|
|
671
|
+
'',
|
|
672
|
+
'- `openprd visual-compare . --reference <效果图> --actual <实现截图>`:输出左右对比 JPG。',
|
|
673
|
+
'- `openprd dev-check . <file...>`:收工回顾 touched code files 的行数状态与下一步动作。',
|
|
674
|
+
'- `openprd standards . --verify`:校验 `docs/basic/`、文件说明书、文件夹 README 等标准。',
|
|
675
|
+
'- `openprd quality . --verify`:生成 HTML 质量评估报告并检查 EVO 门禁。',
|
|
676
|
+
'- `openprd grow . --review`:审查执行中发现的规则/配置候选,再决定是否 apply。',
|
|
677
|
+
'',
|
|
678
|
+
'## 深度扫描与历史项目',
|
|
679
|
+
'',
|
|
680
|
+
'- `openprd discovery . --resume|--verify`:恢复或校验 discovery 状态。',
|
|
681
|
+
'- `openprd fleet <root> --dry-run`:批量审计历史项目。',
|
|
682
|
+
'- `openprd fleet <root> --sync-registry`:把当前 root 下已初始化的 `.openprd/` 工作区回填到全局 registry。',
|
|
683
|
+
'- `openprd fleet <root> --backfill-work-units`:补历史 PRD work unit 绑定。',
|
|
684
|
+
'- `openprd fleet <root> --update-openprd`:只刷新已有 `.openprd/` 的项目。',
|
|
685
|
+
'',
|
|
686
|
+
'## 使用原则',
|
|
687
|
+
'',
|
|
688
|
+
'- 规划、分析、评审、解释影响范围时,保持只读;不要因为命令存在就直接执行写入。',
|
|
689
|
+
'- 只有用户明确要求实现、继续任务、深度调研、对标复刻或提交时,才进入 `tasks --advance`、`loop --run`、commit、push 等执行动作。',
|
|
690
|
+
'- 高风险动作前先过 `openprd standards . --verify`、`openprd quality . --verify` 和 `openprd run . --verify`;`openprd doctor .` 主要用于集成漂移、生成引导 drift,或 commit/push/freeze/handoff 前的最终健康检查。',
|
|
691
|
+
'',
|
|
692
|
+
].join('\n');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function cjoin(...parts) {
|
|
696
|
+
return path.join(...parts);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function exists(filePath) {
|
|
700
|
+
return fs.access(filePath).then(() => true).catch(() => false);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function readText(filePath) {
|
|
704
|
+
return fs.readFile(filePath, 'utf8');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function readJson(filePath) {
|
|
708
|
+
return JSON.parse(await readText(filePath));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function writeText(filePath, text) {
|
|
712
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
713
|
+
await fs.writeFile(filePath, text, 'utf8');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function writeJson(filePath, value) {
|
|
717
|
+
await writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function appendJsonl(filePath, value) {
|
|
721
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
722
|
+
await fs.appendFile(filePath, `${JSON.stringify(value)}\n`, 'utf8');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function packageVersion() {
|
|
726
|
+
const pkg = await readJson(cjoin(PACKAGE_ROOT, 'package.json')).catch(() => ({}));
|
|
727
|
+
return String(pkg.version ?? '0.0.0');
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function checksum(text) {
|
|
731
|
+
return crypto.createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function fileChecksum(filePath) {
|
|
735
|
+
const text = await readText(filePath);
|
|
736
|
+
return checksum(text);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function normalizedRelativePath(projectRoot, filePath) {
|
|
740
|
+
return path.relative(projectRoot, filePath).split(path.sep).join('/');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function harnessPath(projectRoot, relativePath) {
|
|
744
|
+
return cjoin(projectRoot, relativePath);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function recordManagedFile(options, record) {
|
|
748
|
+
if (!Array.isArray(options.managedFiles)) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
options.managedFiles.push(record);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function normalizeTools(value) {
|
|
755
|
+
const raw = String(value ?? 'all').trim().toLowerCase();
|
|
756
|
+
if (!raw || raw === 'all' || raw === 'auto') {
|
|
757
|
+
return OPENPRD_AGENT_TOOLS;
|
|
758
|
+
}
|
|
759
|
+
const tools = raw.split(',').map((item) => item.trim()).filter(Boolean);
|
|
760
|
+
const invalid = tools.filter((tool) => !OPENPRD_AGENT_TOOLS.includes(tool));
|
|
761
|
+
if (invalid.length > 0) {
|
|
762
|
+
throw new Error(`Unsupported OpenPrd agent tool(s): ${invalid.join(', ')}`);
|
|
763
|
+
}
|
|
764
|
+
return [...new Set(tools)];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function normalizeHookProfile(value) {
|
|
768
|
+
const profile = String(value ?? OPENPRD_DEFAULT_HOOK_PROFILE).trim().toLowerCase() || OPENPRD_DEFAULT_HOOK_PROFILE;
|
|
769
|
+
if (!Object.prototype.hasOwnProperty.call(OPENPRD_HOOK_PROFILES, profile)) {
|
|
770
|
+
throw new Error(`Unsupported OpenPrd hook profile: ${profile}. Use lite, guarded, or full.`);
|
|
771
|
+
}
|
|
772
|
+
return profile;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function hookEventsForProfile(profile) {
|
|
776
|
+
return OPENPRD_HOOK_PROFILES[normalizeHookProfile(profile)];
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function codexHookMatcher(eventName, hookProfile) {
|
|
780
|
+
if (!OPENPRD_HOOK_EVENTS_WITH_MATCHER.has(eventName)) return null;
|
|
781
|
+
const profile = normalizeHookProfile(hookProfile);
|
|
782
|
+
if (profile === 'lite' && eventName === 'PreToolUse') {
|
|
783
|
+
return OPENPRD_LITE_WRITE_TOOL_MATCHER;
|
|
784
|
+
}
|
|
785
|
+
if (profile === 'guarded' && eventName === 'PreToolUse') {
|
|
786
|
+
return OPENPRD_GUARDED_WRITE_TOOL_MATCHER;
|
|
787
|
+
}
|
|
788
|
+
return '*';
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function resolveCodexHome(options = {}) {
|
|
792
|
+
return options.codexHome
|
|
793
|
+
?? process.env.OPENPRD_CODEX_HOME
|
|
794
|
+
?? process.env.CODEX_HOME
|
|
795
|
+
?? cjoin(os.homedir(), '.codex');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function displayPath(filePath) {
|
|
799
|
+
const home = os.homedir();
|
|
800
|
+
return filePath.startsWith(`${home}${path.sep}`) ? `~/${path.relative(home, filePath)}` : filePath;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function managedBlock(id, body) {
|
|
804
|
+
return [
|
|
805
|
+
`<!-- OPENPRD:${id}:START -->`,
|
|
806
|
+
body.trimEnd(),
|
|
807
|
+
`<!-- OPENPRD:${id}:END -->`,
|
|
808
|
+
'',
|
|
809
|
+
].join('\n');
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function upsertManagedBlock(text, id, body) {
|
|
813
|
+
const start = `<!-- OPENPRD:${id}:START -->`;
|
|
814
|
+
const end = `<!-- OPENPRD:${id}:END -->`;
|
|
815
|
+
const block = managedBlock(id, body);
|
|
816
|
+
const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, 'm');
|
|
817
|
+
if (pattern.test(text)) {
|
|
818
|
+
return text.replace(pattern, block);
|
|
819
|
+
}
|
|
820
|
+
if (!String(text || '').trim()) {
|
|
821
|
+
return block;
|
|
822
|
+
}
|
|
823
|
+
return `${text.trimEnd()}\n\n${block}`;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function escapeRegExp(value) {
|
|
827
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function generatedMarker({ adapter, source, version, body, commentStyle = 'html' }) {
|
|
831
|
+
const lines = [
|
|
832
|
+
'OPENPRD:GENERATED',
|
|
833
|
+
`adapter=${adapter}`,
|
|
834
|
+
`source=${source}`,
|
|
835
|
+
`version=${version}`,
|
|
836
|
+
`checksum=${checksum(body)}`,
|
|
837
|
+
];
|
|
838
|
+
if (commentStyle === 'js') {
|
|
839
|
+
return `/* ${lines.join('\n')}\n*/`;
|
|
840
|
+
}
|
|
841
|
+
return [
|
|
842
|
+
'<!-- OPENPRD:GENERATED',
|
|
843
|
+
`adapter=${adapter}`,
|
|
844
|
+
`source=${source}`,
|
|
845
|
+
`version=${version}`,
|
|
846
|
+
`checksum=${checksum(body)}`,
|
|
847
|
+
'-->',
|
|
848
|
+
].join('\n');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function renderGeneratedFile(params) {
|
|
852
|
+
const marker = generatedMarker(params);
|
|
853
|
+
const body = params.body.trimEnd();
|
|
854
|
+
if (params.commentStyle === 'js') {
|
|
855
|
+
return `${marker}\n\n${body}\n`;
|
|
856
|
+
}
|
|
857
|
+
if (body.startsWith('---\n')) {
|
|
858
|
+
const frontmatterEnd = body.indexOf('\n---\n', 4);
|
|
859
|
+
if (frontmatterEnd >= 0) {
|
|
860
|
+
const closeEnd = frontmatterEnd + '\n---\n'.length;
|
|
861
|
+
return `${body.slice(0, closeEnd)}\n${marker}\n${body.slice(closeEnd)}\n`;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return `${marker}\n\n${body}\n`;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function writeGeneratedFile(filePath, params, options, changes) {
|
|
868
|
+
const body = renderGeneratedFile(params);
|
|
869
|
+
const rel = normalizedRelativePath(options.projectRoot, filePath);
|
|
870
|
+
const current = await readText(filePath).catch(() => null);
|
|
871
|
+
if (current && !current.includes(OPENPRD_GENERATED_MARKER) && !options.force) {
|
|
872
|
+
changes.push({
|
|
873
|
+
path: rel,
|
|
874
|
+
status: 'skipped-user-file',
|
|
875
|
+
reason: 'missing-generated-marker',
|
|
876
|
+
message: 'Existing file has no OPENPRD:GENERATED marker; preserved as a user file.',
|
|
877
|
+
repairHint: `Review ${rel}; run openprd update . --force to replace it with the canonical OpenPrd generated file if appropriate.`,
|
|
878
|
+
});
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
await writeText(filePath, body);
|
|
882
|
+
changes.push({ path: rel, status: current ? 'updated' : 'created' });
|
|
883
|
+
recordManagedFile(options, {
|
|
884
|
+
path: rel,
|
|
885
|
+
scope: 'project',
|
|
886
|
+
kind: 'generated-file',
|
|
887
|
+
adapter: params.adapter,
|
|
888
|
+
source: params.source,
|
|
889
|
+
bodyChecksum: checksum(params.body),
|
|
890
|
+
fileChecksum: checksum(body),
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function upsertTextBlockFile(filePath, id, blockBody, options, changes) {
|
|
895
|
+
const rel = normalizedRelativePath(options.projectRoot, filePath);
|
|
896
|
+
const current = await readText(filePath).catch(() => '');
|
|
897
|
+
const next = upsertManagedBlock(current || '', id, blockBody);
|
|
898
|
+
await writeText(filePath, next);
|
|
899
|
+
changes.push({ path: rel, status: current ? 'updated-block' : 'created' });
|
|
900
|
+
recordManagedFile(options, {
|
|
901
|
+
path: rel,
|
|
902
|
+
scope: 'project',
|
|
903
|
+
kind: 'managed-block',
|
|
904
|
+
marker: `OPENPRD:${id}`,
|
|
905
|
+
bodyChecksum: checksum(blockBody),
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
async function writeProjectCommandCatalog(projectRoot, options, changes) {
|
|
910
|
+
const version = await packageVersion();
|
|
911
|
+
await writeGeneratedFile(
|
|
912
|
+
cjoin(projectRoot, '.openprd', 'harness', 'command-catalog.md'),
|
|
913
|
+
{ adapter: 'project', source: 'command-catalog', version, body: renderCommandCatalog() },
|
|
914
|
+
options,
|
|
915
|
+
changes,
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function agentContractBody() {
|
|
920
|
+
return [
|
|
921
|
+
'## OpenPrd Harness',
|
|
922
|
+
'',
|
|
923
|
+
'本项目由 OpenPrd 管理。Agent 应优先遵循 repo-local skills 和 hooks;`AGENTS.md` 只保留轻量入口合同。',
|
|
924
|
+
'',
|
|
925
|
+
'### Scope',
|
|
926
|
+
'',
|
|
927
|
+
'- skill 路由放在 `openprd-router`,命令清单放在 command catalog,强约束放在 hooks。',
|
|
928
|
+
'- `AGENTS.md` 只说明入口、默认行为和高风险门禁,不再承载静态长清单。',
|
|
929
|
+
'',
|
|
930
|
+
'### Entry Points',
|
|
931
|
+
'',
|
|
932
|
+
'- 先读 `skills/openprd-router/SKILL.md`;在生成的 Codex / Claude 环境里,优先读同名 `openprd-router` skill。',
|
|
933
|
+
'- 需要具体命令时,优先读 `.openprd/harness/command-catalog.md`,不要继续把命令清单膨胀回 `AGENTS.md`。',
|
|
934
|
+
'- `$openprd-shared`:共用语言、文档影响、敏感信息、浏览器安全、小程序验证、产品文案与 i18n 规则。',
|
|
935
|
+
'- `$openprd-requirement-intake`:需求入口分流、L0/L1/L2 判断、PRD lens 选择。',
|
|
936
|
+
'- `$openprd-harness`:主工作流、`run/loop`、review/change/tasks 与执行节奏。',
|
|
937
|
+
'- `$openprd-benchmark-router`:外部技术、公开 GitHub 仓库、benchmark/对标/最佳实践路由。',
|
|
938
|
+
'- `$openprd-standards` / `$openprd-quality`:`docs/basic/`、就绪验证、EVO 门禁、知识沉淀。',
|
|
939
|
+
'- `$openprd-diagram-review` / `$openprd-discovery-loop`:可视评审与长时间只读挖掘。',
|
|
940
|
+
'',
|
|
941
|
+
'### 默认行为',
|
|
942
|
+
'',
|
|
943
|
+
'1. 动手前先从 `.openprd/` 重建状态,并先运行 `openprd run . --context`;它是建议上下文,不是自动执行指令。',
|
|
944
|
+
'2. 规划、分析、架构评审、“怎么改”或“会动哪些文件”类请求保持只读;只有用户明确要求实现、继续任务、深度调研、对标复刻或提交时才进入执行。',
|
|
945
|
+
'3. 先分流再执行:需求复杂度由 `openprd-requirement-intake` 按影响面、未知数、决策成本和验证成本判断;L0 小修直接处理并事后说明,L1 中等改动先在对话内给 mini-plan 再执行,L2 高影响或边界不清的需求先走 requirement intake,再 `review/change/tasks`,最后才实现。`review.html` 是稳定评审 artifact,不再默认等于唯一的人类停顿点;默认按 decision-points approval policy 执行,只有当前 lane 仍要求人类决策时才在 final answer 主体里停下请求确认。',
|
|
946
|
+
'4. 纯图片、封面图、配图、海报、插画、图标、贴纸、mockup 或“先看样子”请求默认直接使用 Codex 原生 Image 2;其中 logo、icon、avatar、badge 等开发素材在用户未明确要求场景化展示时,默认按独立素材输出(standalone asset)生成:全画布单主体,不额外添加卡片、设备框或其他展示容器;只有进入实现阶段且已有参考图时,才使用 `openprd visual-compare`。',
|
|
947
|
+
'5. 用户给出会话 ID 并要求继续时,按工具无关的历史会话续接;不要要求工具专属 ID,也不要用当前 active change 或相似历史替代指定会话。',
|
|
948
|
+
'6. 代码修改完成后、最终回复前,针对本轮实际 touched code files 运行 `openprd dev-check . <file...>`;宣称准备就绪前,运行 `openprd standards . --verify`、`openprd quality . --verify` 和 `openprd run . --verify`。',
|
|
949
|
+
'',
|
|
950
|
+
'### Hook-Enforced Gates',
|
|
951
|
+
'',
|
|
952
|
+
'- requirement:需求未完成 `clarify/review/change/tasks` 前阻断实现写入;tasks 就绪后,只有用户原始意图已明确要求实现,或后续明确发出执行指令时才放行。',
|
|
953
|
+
'- research:公开 GitHub 架构/对标先 DeepWiki;第三方技术用法、配置、限制、版本差异或迁移先查本地证据,不足时再按 `resolve_library_id -> query_docs` 使用 Context7。',
|
|
954
|
+
'- skill-visualization:修改 skill、`SKILL.md`、`AGENTS.md` 或相关 workflow 前,先输出彩色 Mermaid 方案并等待用户确认。',
|
|
955
|
+
'- secrets / weapp / browser / copy:分别处理 `secrets-vault`、`weapp-dev-mcp`、窗口归属与 i18n/普通用户文案提醒。',
|
|
956
|
+
'- 需要细节时,读 router 指向的 skill 和 command catalog,而不是继续扩写 `AGENTS.md`。',
|
|
957
|
+
'',
|
|
958
|
+
'### High-Risk Gate',
|
|
959
|
+
'',
|
|
960
|
+
'Before freeze, handoff, accepted spec apply/archive, commit, push, release, or publish, ensure `openprd standards . --verify`, `openprd quality . --verify`, `openprd run . --verify`, and `openprd doctor .` are healthy.',
|
|
961
|
+
'If the quality report says `productionReady=false`, do not claim readiness; list the missing evidence or gates.',
|
|
962
|
+
'The only baseline documentation path is `docs/basic/`.',
|
|
963
|
+
'',
|
|
964
|
+
].join('\n');
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function parseSkillSourceMarkdown(text) {
|
|
968
|
+
const raw = String(text ?? '').trimEnd();
|
|
969
|
+
if (!raw.startsWith('---\n')) {
|
|
970
|
+
return { frontmatter: {}, body: raw };
|
|
971
|
+
}
|
|
972
|
+
const frontmatterEnd = raw.indexOf('\n---\n', 4);
|
|
973
|
+
if (frontmatterEnd < 0) {
|
|
974
|
+
return { frontmatter: {}, body: raw };
|
|
975
|
+
}
|
|
976
|
+
const frontmatterText = raw.slice(4, frontmatterEnd).trim();
|
|
977
|
+
const body = raw.slice(frontmatterEnd + '\n---\n'.length).trimStart();
|
|
978
|
+
const frontmatter = {};
|
|
979
|
+
for (const line of frontmatterText.split(/\r?\n/)) {
|
|
980
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
981
|
+
if (match) {
|
|
982
|
+
frontmatter[match[1]] = match[2].trim();
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return { frontmatter, body };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function loadSourceSkill(skill) {
|
|
989
|
+
if (!skill.sourceSkill) {
|
|
990
|
+
return { description: skill.description, body: skill.body };
|
|
991
|
+
}
|
|
992
|
+
const sourcePath = cjoin(PACKAGE_ROOT, 'skills', skill.sourceSkill, 'SKILL.md');
|
|
993
|
+
const parsed = parseSkillSourceMarkdown(await readText(sourcePath));
|
|
994
|
+
return {
|
|
995
|
+
description: skill.description ?? parsed.frontmatter.description,
|
|
996
|
+
body: skill.body ?? parsed.body,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function renderSkill(skill, adapter, projectRoot) {
|
|
1001
|
+
const source = await loadSourceSkill(skill);
|
|
1002
|
+
const sections = [
|
|
1003
|
+
'---',
|
|
1004
|
+
`name: ${skill.id}`,
|
|
1005
|
+
`description: ${source.description}`,
|
|
1006
|
+
'---',
|
|
1007
|
+
'',
|
|
1008
|
+
source.body,
|
|
1009
|
+
];
|
|
1010
|
+
if (skill.id === 'openprd-benchmark-router' && projectRoot) {
|
|
1011
|
+
sections.push('', await renderApprovedBenchmarkRegistrySection(projectRoot).catch(() => ''));
|
|
1012
|
+
}
|
|
1013
|
+
return sections.join('\n');
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
async function listFilesRecursive(rootDir) {
|
|
1017
|
+
const entries = await fs.readdir(rootDir, { withFileTypes: true }).catch((error) => {
|
|
1018
|
+
if (error?.code === 'ENOENT') return [];
|
|
1019
|
+
throw error;
|
|
1020
|
+
});
|
|
1021
|
+
const files = [];
|
|
1022
|
+
for (const entry of entries) {
|
|
1023
|
+
const itemPath = cjoin(rootDir, entry.name);
|
|
1024
|
+
if (entry.isDirectory()) {
|
|
1025
|
+
files.push(...await listFilesRecursive(itemPath));
|
|
1026
|
+
} else if (entry.isFile()) {
|
|
1027
|
+
files.push(itemPath);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return files;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function writeSourceSkillReferences(projectRoot, skill, destinationDir, adapter, version, options, changes) {
|
|
1034
|
+
if (!skill.sourceSkill) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const sourceDir = cjoin(PACKAGE_ROOT, 'skills', skill.sourceSkill, 'references');
|
|
1038
|
+
const files = await listFilesRecursive(sourceDir);
|
|
1039
|
+
for (const filePath of files) {
|
|
1040
|
+
const relative = path.relative(sourceDir, filePath).split(path.sep).join('/');
|
|
1041
|
+
const body = await readText(filePath);
|
|
1042
|
+
await writeGeneratedFile(
|
|
1043
|
+
cjoin(destinationDir, 'references', relative),
|
|
1044
|
+
{ adapter, source: `${skill.id}:reference:${relative}`, version, body },
|
|
1045
|
+
options,
|
|
1046
|
+
changes,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function renderCommand(command, adapter) {
|
|
1052
|
+
if (adapter === 'cursor') {
|
|
1053
|
+
return [
|
|
1054
|
+
`# ${command.title}`,
|
|
1055
|
+
'',
|
|
1056
|
+
command.body,
|
|
1057
|
+
'',
|
|
1058
|
+
'Always follow the OpenPrd managed rules in `.cursor/rules/openprd.mdc` and project `AGENTS.md`.',
|
|
1059
|
+
'',
|
|
1060
|
+
].join('\n');
|
|
1061
|
+
}
|
|
1062
|
+
return [
|
|
1063
|
+
`# ${command.title}`,
|
|
1064
|
+
'',
|
|
1065
|
+
command.body,
|
|
1066
|
+
'',
|
|
1067
|
+
'Always rebuild state from `.openprd/` before acting.',
|
|
1068
|
+
'',
|
|
1069
|
+
].join('\n');
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function renderCursorRule() {
|
|
1073
|
+
return [
|
|
1074
|
+
'---',
|
|
1075
|
+
'description: OpenPrd harness 规则',
|
|
1076
|
+
'globs:',
|
|
1077
|
+
' - "**/*"',
|
|
1078
|
+
'alwaysApply: true',
|
|
1079
|
+
'---',
|
|
1080
|
+
'',
|
|
1081
|
+
'# OpenPrd Harness',
|
|
1082
|
+
'',
|
|
1083
|
+
agentContractBody(),
|
|
1084
|
+
].join('\n');
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function quoteForToml(value) {
|
|
1088
|
+
return JSON.stringify(value);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function hookCommand(projectRoot, eventName) {
|
|
1092
|
+
const hookPath = cjoin(projectRoot, '.codex', 'hooks', 'openprd-hook.mjs');
|
|
1093
|
+
return `node ${quoteShell(hookPath)} ${eventName}`;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function quoteShell(value) {
|
|
1097
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function renderCodexHookRunner() {
|
|
1101
|
+
return readFileSync(cjoin(PACKAGE_ROOT, 'src', 'codex-hook-runner-template.mjs'), 'utf8').trimEnd();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function ensureFeatureHooks(text) {
|
|
1105
|
+
const featureHeader = /^\[features\]\s*$/m;
|
|
1106
|
+
if (!featureHeader.test(text)) {
|
|
1107
|
+
const prefix = text.trimEnd();
|
|
1108
|
+
return `${prefix ? `${prefix}\n\n` : ''}[features]\ncodex_hooks = true\n`;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const lines = text.split(/\r?\n/);
|
|
1112
|
+
const start = lines.findIndex((line) => line.trim() === '[features]');
|
|
1113
|
+
let end = lines.length;
|
|
1114
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
1115
|
+
if (/^\[.+\]\s*$/.test(lines[index].trim())) {
|
|
1116
|
+
end = index;
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
let hasHooks = false;
|
|
1121
|
+
const legacyHookLines = [];
|
|
1122
|
+
for (let index = start + 1; index < end; index += 1) {
|
|
1123
|
+
if (/^\s*codex_hooks\s*=/.test(lines[index])) {
|
|
1124
|
+
lines[index] = 'codex_hooks = true';
|
|
1125
|
+
hasHooks = true;
|
|
1126
|
+
} else if (/^\s*hooks\s*=/.test(lines[index])) {
|
|
1127
|
+
legacyHookLines.push(index);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (hasHooks) {
|
|
1131
|
+
for (let index = legacyHookLines.length - 1; index >= 0; index -= 1) {
|
|
1132
|
+
lines.splice(legacyHookLines[index], 1);
|
|
1133
|
+
}
|
|
1134
|
+
} else if (legacyHookLines.length > 0) {
|
|
1135
|
+
lines[legacyHookLines[0]] = 'codex_hooks = true';
|
|
1136
|
+
for (let index = legacyHookLines.length - 1; index >= 1; index -= 1) {
|
|
1137
|
+
lines.splice(legacyHookLines[index], 1);
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
lines.splice(end, 0, 'codex_hooks = true');
|
|
1141
|
+
}
|
|
1142
|
+
return lines.join('\n');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function codexHooksTomlBlock(projectRoot, options = {}) {
|
|
1146
|
+
const events = hookEventsForProfile(options.hookProfile);
|
|
1147
|
+
const groups = [];
|
|
1148
|
+
for (const eventName of events) {
|
|
1149
|
+
groups.push(`[[hooks.${eventName}]]`);
|
|
1150
|
+
const matcher = codexHookMatcher(eventName, options.hookProfile);
|
|
1151
|
+
if (matcher) {
|
|
1152
|
+
groups.push(`matcher = ${quoteForToml(matcher)}`);
|
|
1153
|
+
}
|
|
1154
|
+
groups.push(`hooks = [{ type = "command", command = ${quoteForToml(hookCommand(projectRoot, eventName))}, timeout = 15000 }]`);
|
|
1155
|
+
groups.push('');
|
|
1156
|
+
}
|
|
1157
|
+
return groups.join('\n').trimEnd();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function upsertTomlManagedBlock(text, id, body) {
|
|
1161
|
+
const start = `# OPENPRD:${id}:START`;
|
|
1162
|
+
const end = `# OPENPRD:${id}:END`;
|
|
1163
|
+
const block = `${start}\n${body.trimEnd()}\n${end}\n`;
|
|
1164
|
+
const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, 'm');
|
|
1165
|
+
if (pattern.test(text)) {
|
|
1166
|
+
return text.replace(pattern, block);
|
|
1167
|
+
}
|
|
1168
|
+
return `${text.trimEnd()}\n\n${block}`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async function writeCodexConfig(projectRoot, options, changes) {
|
|
1172
|
+
const configPath = cjoin(projectRoot, '.codex', 'config.toml');
|
|
1173
|
+
const rel = normalizedRelativePath(projectRoot, configPath);
|
|
1174
|
+
const current = await readText(configPath).catch(() => '');
|
|
1175
|
+
let next = ensureFeatureHooks(current || '');
|
|
1176
|
+
next = upsertTomlManagedBlock(next, 'CODEX-HOOKS', codexHooksTomlBlock(projectRoot, options));
|
|
1177
|
+
await writeText(configPath, next);
|
|
1178
|
+
changes.push({ path: rel, status: current ? 'updated' : 'created' });
|
|
1179
|
+
recordManagedFile(options, {
|
|
1180
|
+
path: rel,
|
|
1181
|
+
scope: 'project',
|
|
1182
|
+
kind: 'codex-config',
|
|
1183
|
+
marker: 'OPENPRD:CODEX-HOOKS',
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
async function writeCodexUserConfig(options, changes) {
|
|
1188
|
+
const configPath = cjoin(resolveCodexHome(options), 'config.toml');
|
|
1189
|
+
const current = await readText(configPath).catch(() => '');
|
|
1190
|
+
const next = ensureFeatureHooks(current || '');
|
|
1191
|
+
if (next !== current) {
|
|
1192
|
+
await writeText(configPath, next);
|
|
1193
|
+
}
|
|
1194
|
+
changes.push({
|
|
1195
|
+
path: displayPath(configPath),
|
|
1196
|
+
status: current ? (next === current ? 'unchanged' : 'updated') : 'created',
|
|
1197
|
+
});
|
|
1198
|
+
recordManagedFile(options, {
|
|
1199
|
+
path: displayPath(configPath),
|
|
1200
|
+
scope: 'user',
|
|
1201
|
+
kind: 'codex-user-config',
|
|
1202
|
+
marker: 'codex_hooks = true',
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function codexHookGroup(projectRoot, eventName, options = {}) {
|
|
1207
|
+
const group = {
|
|
1208
|
+
hooks: [
|
|
1209
|
+
{
|
|
1210
|
+
type: 'command',
|
|
1211
|
+
command: hookCommand(projectRoot, eventName),
|
|
1212
|
+
timeout: 15000,
|
|
1213
|
+
},
|
|
1214
|
+
],
|
|
1215
|
+
};
|
|
1216
|
+
const matcher = codexHookMatcher(eventName, options.hookProfile);
|
|
1217
|
+
if (matcher) {
|
|
1218
|
+
group.matcher = matcher;
|
|
1219
|
+
}
|
|
1220
|
+
return group;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function isOpenPrdHookGroup(group) {
|
|
1224
|
+
return JSON.stringify(group ?? {}).includes('openprd-hook.mjs');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function writeCodexHooksJson(projectRoot, options, changes) {
|
|
1228
|
+
const hooksPath = cjoin(projectRoot, '.codex', 'hooks.json');
|
|
1229
|
+
const rel = normalizedRelativePath(projectRoot, hooksPath);
|
|
1230
|
+
const existed = await exists(hooksPath);
|
|
1231
|
+
const current = existed ? await readJson(hooksPath).catch(() => ({})) : {};
|
|
1232
|
+
const next = current && typeof current === 'object' && !Array.isArray(current) ? current : {};
|
|
1233
|
+
const activeEvents = new Set(hookEventsForProfile(options.hookProfile));
|
|
1234
|
+
for (const eventName of OPENPRD_EVENTS) {
|
|
1235
|
+
const existing = Array.isArray(next[eventName]) ? next[eventName] : [];
|
|
1236
|
+
const kept = existing.filter((group) => !isOpenPrdHookGroup(group));
|
|
1237
|
+
if (activeEvents.has(eventName)) {
|
|
1238
|
+
next[eventName] = [
|
|
1239
|
+
...kept,
|
|
1240
|
+
codexHookGroup(projectRoot, eventName, options),
|
|
1241
|
+
];
|
|
1242
|
+
} else if (kept.length > 0) {
|
|
1243
|
+
next[eventName] = kept;
|
|
1244
|
+
} else {
|
|
1245
|
+
delete next[eventName];
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
await writeJson(hooksPath, next);
|
|
1249
|
+
changes.push({ path: rel, status: existed ? 'updated' : 'created' });
|
|
1250
|
+
recordManagedFile(options, {
|
|
1251
|
+
path: rel,
|
|
1252
|
+
scope: 'project',
|
|
1253
|
+
kind: 'codex-hooks-json',
|
|
1254
|
+
marker: 'openprd-hook.mjs',
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
async function writeCodexAdapter(projectRoot, options, changes) {
|
|
1259
|
+
const version = await packageVersion();
|
|
1260
|
+
for (const skill of CANONICAL_SKILLS) {
|
|
1261
|
+
const body = await renderSkill(skill, 'codex', projectRoot);
|
|
1262
|
+
const skillDir = cjoin(projectRoot, '.codex', 'skills', skill.id);
|
|
1263
|
+
await writeGeneratedFile(
|
|
1264
|
+
cjoin(skillDir, 'SKILL.md'),
|
|
1265
|
+
{ adapter: 'codex', source: skill.id, version, body },
|
|
1266
|
+
options,
|
|
1267
|
+
changes,
|
|
1268
|
+
);
|
|
1269
|
+
await writeSourceSkillReferences(projectRoot, skill, skillDir, 'codex', version, options, changes);
|
|
1270
|
+
}
|
|
1271
|
+
for (const command of CANONICAL_COMMANDS) {
|
|
1272
|
+
await writeGeneratedFile(
|
|
1273
|
+
cjoin(projectRoot, '.codex', 'prompts', `openprd-${command.id}.md`),
|
|
1274
|
+
{ adapter: 'codex', source: `command:${command.id}`, version, body: renderCommand(command, 'codex') },
|
|
1275
|
+
options,
|
|
1276
|
+
changes,
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
const hookPath = cjoin(projectRoot, '.codex', 'hooks', 'openprd-hook.mjs');
|
|
1280
|
+
await writeGeneratedFile(
|
|
1281
|
+
hookPath,
|
|
1282
|
+
{ adapter: 'codex', source: 'codex-hooks', version, body: renderCodexHookRunner(), commentStyle: 'js' },
|
|
1283
|
+
{ ...options, force: true },
|
|
1284
|
+
changes,
|
|
1285
|
+
);
|
|
1286
|
+
await fs.chmod(hookPath, 0o755).catch(() => {});
|
|
1287
|
+
await writeCodexConfig(projectRoot, options, changes);
|
|
1288
|
+
await writeCodexHooksJson(projectRoot, options, changes);
|
|
1289
|
+
if (options.enableUserCodexConfig) {
|
|
1290
|
+
await writeCodexUserConfig(options, changes);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async function writeClaudeAdapter(projectRoot, options, changes) {
|
|
1295
|
+
const version = await packageVersion();
|
|
1296
|
+
await upsertTextBlockFile(cjoin(projectRoot, 'CLAUDE.md'), 'CLAUDE', agentContractBody(), options, changes);
|
|
1297
|
+
for (const skill of CANONICAL_SKILLS) {
|
|
1298
|
+
const body = await renderSkill(skill, 'claude', projectRoot);
|
|
1299
|
+
const skillDir = cjoin(projectRoot, '.claude', 'skills', skill.id);
|
|
1300
|
+
await writeGeneratedFile(
|
|
1301
|
+
cjoin(skillDir, 'SKILL.md'),
|
|
1302
|
+
{ adapter: 'claude', source: skill.id, version, body },
|
|
1303
|
+
options,
|
|
1304
|
+
changes,
|
|
1305
|
+
);
|
|
1306
|
+
await writeSourceSkillReferences(projectRoot, skill, skillDir, 'claude', version, options, changes);
|
|
1307
|
+
}
|
|
1308
|
+
for (const command of CANONICAL_COMMANDS) {
|
|
1309
|
+
await writeGeneratedFile(
|
|
1310
|
+
cjoin(projectRoot, '.claude', 'commands', 'openprd', `${command.id}.md`),
|
|
1311
|
+
{ adapter: 'claude', source: `command:${command.id}`, version, body: renderCommand(command, 'claude') },
|
|
1312
|
+
options,
|
|
1313
|
+
changes,
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
async function writeCursorAdapter(projectRoot, options, changes) {
|
|
1319
|
+
const version = await packageVersion();
|
|
1320
|
+
await writeGeneratedFile(
|
|
1321
|
+
cjoin(projectRoot, '.cursor', 'rules', 'openprd.mdc'),
|
|
1322
|
+
{ adapter: 'cursor', source: 'cursor-rules', version, body: renderCursorRule() },
|
|
1323
|
+
options,
|
|
1324
|
+
changes,
|
|
1325
|
+
);
|
|
1326
|
+
for (const command of CANONICAL_COMMANDS) {
|
|
1327
|
+
await writeGeneratedFile(
|
|
1328
|
+
cjoin(projectRoot, '.cursor', 'commands', `openprd-${command.id}.md`),
|
|
1329
|
+
{ adapter: 'cursor', source: `command:${command.id}`, version, body: renderCommand(command, 'cursor') },
|
|
1330
|
+
options,
|
|
1331
|
+
changes,
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function ensureHarnessState(projectRoot) {
|
|
1337
|
+
await fs.mkdir(harnessPath(projectRoot, OPENPRD_HARNESS_DIR), { recursive: true });
|
|
1338
|
+
const hookStatePath = harnessPath(projectRoot, OPENPRD_HARNESS_HOOK_STATE);
|
|
1339
|
+
if (!(await exists(hookStatePath))) {
|
|
1340
|
+
await writeJson(hookStatePath, {
|
|
1341
|
+
version: 1,
|
|
1342
|
+
active: true,
|
|
1343
|
+
lastEventAt: null,
|
|
1344
|
+
lastFingerprint: null,
|
|
1345
|
+
counters: {},
|
|
1346
|
+
suppressions: {
|
|
1347
|
+
inputLock: false,
|
|
1348
|
+
},
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
const eventsPath = harnessPath(projectRoot, OPENPRD_HARNESS_EVENTS);
|
|
1352
|
+
if (!(await exists(eventsPath))) {
|
|
1353
|
+
await writeText(eventsPath, '');
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
async function writeInstallManifest(projectRoot, options, changes, tools) {
|
|
1358
|
+
const version = await packageVersion();
|
|
1359
|
+
const managedFiles = Array.isArray(options.managedFiles) ? options.managedFiles : [];
|
|
1360
|
+
const hookProfile = normalizeHookProfile(options.hookProfile);
|
|
1361
|
+
const manifest = {
|
|
1362
|
+
version: 1,
|
|
1363
|
+
openprdVersion: version,
|
|
1364
|
+
action: options.action ?? 'setup',
|
|
1365
|
+
generatedAt: timestamp(),
|
|
1366
|
+
tools,
|
|
1367
|
+
managedFiles,
|
|
1368
|
+
hooks: {
|
|
1369
|
+
profile: hookProfile,
|
|
1370
|
+
events: hookEventsForProfile(hookProfile),
|
|
1371
|
+
availableProfiles: Object.keys(OPENPRD_HOOK_PROFILES),
|
|
1372
|
+
state: OPENPRD_HARNESS_HOOK_STATE,
|
|
1373
|
+
eventsLog: OPENPRD_HARNESS_EVENTS,
|
|
1374
|
+
driftReport: OPENPRD_HARNESS_DRIFT,
|
|
1375
|
+
},
|
|
1376
|
+
};
|
|
1377
|
+
const manifestPath = harnessPath(projectRoot, OPENPRD_HARNESS_MANIFEST);
|
|
1378
|
+
const existed = await exists(manifestPath);
|
|
1379
|
+
await writeJson(manifestPath, manifest);
|
|
1380
|
+
changes.push({ path: OPENPRD_HARNESS_MANIFEST, status: existed ? 'updated' : 'created' });
|
|
1381
|
+
await appendJsonl(harnessPath(projectRoot, OPENPRD_HARNESS_EVENTS), {
|
|
1382
|
+
at: manifest.generatedAt,
|
|
1383
|
+
event: 'agent-integration-installed',
|
|
1384
|
+
action: manifest.action,
|
|
1385
|
+
tools,
|
|
1386
|
+
hookProfile,
|
|
1387
|
+
managedFileCount: managedFiles.length,
|
|
1388
|
+
});
|
|
1389
|
+
return manifest;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async function readInstallManifest(projectRoot) {
|
|
1393
|
+
return readJson(harnessPath(projectRoot, OPENPRD_HARNESS_MANIFEST)).catch(() => null);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
async function inspectManagedFile(projectRoot, entry) {
|
|
1397
|
+
if (!entry || entry.scope === 'user') {
|
|
1398
|
+
return { ...entry, ok: true, skipped: true, reason: 'external-user-scope' };
|
|
1399
|
+
}
|
|
1400
|
+
const absolutePath = cjoin(projectRoot, entry.path);
|
|
1401
|
+
const fileExists = await exists(absolutePath);
|
|
1402
|
+
if (!fileExists) {
|
|
1403
|
+
return { ...entry, ok: false, reason: 'missing' };
|
|
1404
|
+
}
|
|
1405
|
+
const text = await readText(absolutePath);
|
|
1406
|
+
if (entry.marker && !text.includes(entry.marker)) {
|
|
1407
|
+
return { ...entry, ok: false, reason: 'missing-marker' };
|
|
1408
|
+
}
|
|
1409
|
+
if (entry.fileChecksum && checksum(text) !== entry.fileChecksum) {
|
|
1410
|
+
return { ...entry, ok: false, reason: 'checksum-drift' };
|
|
1411
|
+
}
|
|
1412
|
+
return { ...entry, ok: true, reason: 'ok' };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
async function computeDriftReport(projectRoot, tools) {
|
|
1416
|
+
const manifest = await readInstallManifest(projectRoot);
|
|
1417
|
+
const checks = [];
|
|
1418
|
+
if (!manifest) {
|
|
1419
|
+
const report = {
|
|
1420
|
+
ok: false,
|
|
1421
|
+
checkedAt: timestamp(),
|
|
1422
|
+
tools,
|
|
1423
|
+
errors: [`${OPENPRD_HARNESS_MANIFEST} is missing. Run openprd update .`],
|
|
1424
|
+
checks,
|
|
1425
|
+
};
|
|
1426
|
+
await writeJson(harnessPath(projectRoot, OPENPRD_HARNESS_DRIFT), report);
|
|
1427
|
+
return report;
|
|
1428
|
+
}
|
|
1429
|
+
for (const entry of manifest.managedFiles ?? []) {
|
|
1430
|
+
checks.push(await inspectManagedFile(projectRoot, entry));
|
|
1431
|
+
}
|
|
1432
|
+
const errors = checks
|
|
1433
|
+
.filter((check) => !check.ok)
|
|
1434
|
+
.map((check) => `${check.path}: ${check.reason}`);
|
|
1435
|
+
const report = {
|
|
1436
|
+
ok: errors.length === 0,
|
|
1437
|
+
checkedAt: timestamp(),
|
|
1438
|
+
tools,
|
|
1439
|
+
manifestVersion: manifest.version,
|
|
1440
|
+
generatedAt: manifest.generatedAt,
|
|
1441
|
+
errors,
|
|
1442
|
+
checks,
|
|
1443
|
+
};
|
|
1444
|
+
await writeJson(harnessPath(projectRoot, OPENPRD_HARNESS_DRIFT), report);
|
|
1445
|
+
return report;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
export async function setupOpenPrdAgentIntegration(projectRoot, options = {}) {
|
|
1449
|
+
const tools = normalizeTools(options.tools);
|
|
1450
|
+
const hookProfile = normalizeHookProfile(options.hookProfile);
|
|
1451
|
+
const changes = [];
|
|
1452
|
+
const managedFiles = [];
|
|
1453
|
+
const normalizedOptions = {
|
|
1454
|
+
...options,
|
|
1455
|
+
projectRoot,
|
|
1456
|
+
hookProfile,
|
|
1457
|
+
managedFiles,
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
await ensureHarnessState(projectRoot);
|
|
1461
|
+
await writeProjectCommandCatalog(projectRoot, normalizedOptions, changes);
|
|
1462
|
+
await upsertTextBlockFile(cjoin(projectRoot, 'AGENTS.md'), 'AGENTS', agentContractBody(), normalizedOptions, changes);
|
|
1463
|
+
|
|
1464
|
+
if (tools.includes('codex')) {
|
|
1465
|
+
await writeCodexAdapter(projectRoot, normalizedOptions, changes);
|
|
1466
|
+
}
|
|
1467
|
+
if (tools.includes('claude')) {
|
|
1468
|
+
await writeClaudeAdapter(projectRoot, normalizedOptions, changes);
|
|
1469
|
+
}
|
|
1470
|
+
if (tools.includes('cursor')) {
|
|
1471
|
+
await writeCursorAdapter(projectRoot, normalizedOptions, changes);
|
|
1472
|
+
}
|
|
1473
|
+
const manifest = await writeInstallManifest(projectRoot, normalizedOptions, changes, tools);
|
|
1474
|
+
const registry = await upsertWorkspaceRegistryEntry(projectRoot, {
|
|
1475
|
+
openprdHome: options.openprdHome,
|
|
1476
|
+
manifest,
|
|
1477
|
+
action: options.action ?? 'setup',
|
|
1478
|
+
tools,
|
|
1479
|
+
hookProfile,
|
|
1480
|
+
recordedAt: manifest.generatedAt,
|
|
1481
|
+
});
|
|
1482
|
+
await appendJsonl(harnessPath(projectRoot, OPENPRD_HARNESS_EVENTS), {
|
|
1483
|
+
at: registry.entry.lastRegisteredAt,
|
|
1484
|
+
event: 'workspace-registry-updated',
|
|
1485
|
+
registryPath: registry.registryPath,
|
|
1486
|
+
status: registry.status,
|
|
1487
|
+
knownTotal: registry.knownTotal,
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
const doctor = await doctorOpenPrdAgentIntegration(projectRoot, {
|
|
1491
|
+
tools,
|
|
1492
|
+
enableUserCodexConfig: Boolean(options.enableUserCodexConfig),
|
|
1493
|
+
codexHome: options.codexHome,
|
|
1494
|
+
hookProfile,
|
|
1495
|
+
});
|
|
1496
|
+
return {
|
|
1497
|
+
ok: doctor.ok,
|
|
1498
|
+
action: options.action ?? 'setup',
|
|
1499
|
+
projectRoot,
|
|
1500
|
+
tools,
|
|
1501
|
+
hookProfile,
|
|
1502
|
+
changes,
|
|
1503
|
+
manifest,
|
|
1504
|
+
registry,
|
|
1505
|
+
doctor,
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
export async function updateOpenPrdAgentIntegration(projectRoot, options = {}) {
|
|
1510
|
+
return setupOpenPrdAgentIntegration(projectRoot, { ...options, action: 'update' });
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
async function fileHas(filePath, pattern) {
|
|
1514
|
+
const text = await readText(filePath).catch(() => '');
|
|
1515
|
+
return text.includes(pattern);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
async function collectDoctorCheck(projectRoot, checks, pathName, predicate, message) {
|
|
1519
|
+
const absolutePath = cjoin(projectRoot, pathName);
|
|
1520
|
+
const ok = await predicate(absolutePath);
|
|
1521
|
+
checks.push({ path: pathName, ok, reason: ok ? 'ok' : 'check-failed', message: ok ? 'ok' : message });
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async function collectDoctorCheckAbsolute(checks, pathName, absolutePath, predicate, message) {
|
|
1525
|
+
const ok = await predicate(absolutePath);
|
|
1526
|
+
checks.push({ path: pathName, ok, reason: ok ? 'ok' : 'check-failed', message: ok ? 'ok' : message });
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
async function collectGeneratedDoctorCheck(projectRoot, checks, pathName, description) {
|
|
1530
|
+
const absolutePath = cjoin(projectRoot, pathName);
|
|
1531
|
+
if (!(await exists(absolutePath))) {
|
|
1532
|
+
checks.push({
|
|
1533
|
+
path: pathName,
|
|
1534
|
+
ok: false,
|
|
1535
|
+
kind: 'generated-file',
|
|
1536
|
+
reason: 'missing-file',
|
|
1537
|
+
message: `${description} is missing.`,
|
|
1538
|
+
repairHint: 'Run openprd update . to regenerate missing OpenPrd guidance.',
|
|
1539
|
+
});
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const text = await readText(absolutePath);
|
|
1544
|
+
if (!text.includes(OPENPRD_GENERATED_MARKER)) {
|
|
1545
|
+
checks.push({
|
|
1546
|
+
path: pathName,
|
|
1547
|
+
ok: false,
|
|
1548
|
+
kind: 'generated-file',
|
|
1549
|
+
reason: 'missing-generated-marker',
|
|
1550
|
+
message: `${description} exists but is not recognized as an OpenPrd generated file because it lacks OPENPRD:GENERATED.`,
|
|
1551
|
+
repairHint: 'Review the local file; if it should be managed by OpenPrd, run openprd update . --force to regenerate it.',
|
|
1552
|
+
});
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
checks.push({ path: pathName, ok: true, kind: 'generated-file', reason: 'ok', message: 'ok' });
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function codexHookSmokePayload(projectRoot, eventName, smokeId) {
|
|
1560
|
+
if (eventName === 'SessionStart') {
|
|
1561
|
+
return { cwd: projectRoot, session_id: smokeId };
|
|
1562
|
+
}
|
|
1563
|
+
if (eventName === 'UserPromptSubmit') {
|
|
1564
|
+
return { cwd: projectRoot, prompt: `OpenPrd doctor hook smoke ${smokeId}` };
|
|
1565
|
+
}
|
|
1566
|
+
if (eventName === 'PreToolUse') {
|
|
1567
|
+
return {
|
|
1568
|
+
cwd: projectRoot,
|
|
1569
|
+
tool_name: 'Read',
|
|
1570
|
+
tool_input: {
|
|
1571
|
+
file_path: '.openprd/state/current.json',
|
|
1572
|
+
},
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
if (eventName === 'PostToolUse') {
|
|
1576
|
+
return {
|
|
1577
|
+
cwd: projectRoot,
|
|
1578
|
+
tool_name: 'Read',
|
|
1579
|
+
tool_input: {
|
|
1580
|
+
file_path: '.openprd/state/current.json',
|
|
1581
|
+
},
|
|
1582
|
+
tool_response: {},
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
return { cwd: projectRoot };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function validateCodexHookSmokeOutput(eventName, run) {
|
|
1589
|
+
if (run.error) {
|
|
1590
|
+
return `Codex hook smoke failed for ${eventName}: ${run.error.message}`;
|
|
1591
|
+
}
|
|
1592
|
+
if (run.status !== 0) {
|
|
1593
|
+
return `Codex hook smoke failed for ${eventName}: exit ${run.status}; ${String(run.stderr ?? '').trim() || 'no stderr'}`;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
let output;
|
|
1597
|
+
try {
|
|
1598
|
+
output = JSON.parse(String(run.stdout ?? '').trim());
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
return `Codex hook smoke emitted invalid JSON for ${eventName}: ${error.message}`;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (!output || typeof output !== 'object' || Array.isArray(output)) {
|
|
1604
|
+
return `Codex hook smoke emitted a non-object payload for ${eventName}.`;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const legacyFields = LEGACY_CODEX_HOOK_OUTPUT_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(output, field));
|
|
1608
|
+
if (legacyFields.length > 0) {
|
|
1609
|
+
return `Codex hook smoke emitted legacy fields for ${eventName}: ${legacyFields.join(', ')}`;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
if (output.continue !== true) {
|
|
1613
|
+
return `Codex hook smoke omitted continue=true for ${eventName}.`;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (output.hookSpecificOutput !== undefined) {
|
|
1617
|
+
if (!output.hookSpecificOutput || typeof output.hookSpecificOutput !== 'object' || Array.isArray(output.hookSpecificOutput)) {
|
|
1618
|
+
return `Codex hook smoke emitted invalid hookSpecificOutput for ${eventName}.`;
|
|
1619
|
+
}
|
|
1620
|
+
if (output.hookSpecificOutput.hookEventName !== eventName) {
|
|
1621
|
+
return `Codex hook smoke emitted hookSpecificOutput.hookEventName=${output.hookSpecificOutput.hookEventName ?? 'null'} for ${eventName}.`;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
async function smokeTestCodexHook(projectRoot, options = {}) {
|
|
1629
|
+
const hookPath = cjoin(projectRoot, '.codex', 'hooks', 'openprd-hook.mjs');
|
|
1630
|
+
if (!(await exists(hookPath))) {
|
|
1631
|
+
return { ok: false, message: 'Codex hook runner is missing.' };
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const smokeId = `doctor-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1635
|
+
const smokeRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'openprd-hook-smoke-'));
|
|
1636
|
+
try {
|
|
1637
|
+
for (const eventName of hookEventsForProfile(options.hookProfile)) {
|
|
1638
|
+
const run = spawnSync(process.execPath, [hookPath, eventName], {
|
|
1639
|
+
cwd: smokeRoot,
|
|
1640
|
+
input: JSON.stringify(codexHookSmokePayload(smokeRoot, eventName, smokeId)),
|
|
1641
|
+
encoding: 'utf8',
|
|
1642
|
+
timeout: 15000,
|
|
1643
|
+
env: {
|
|
1644
|
+
...process.env,
|
|
1645
|
+
OPENPRD_CLI: process.env.OPENPRD_CLI || cjoin(PACKAGE_ROOT, 'bin', 'openprd.js'),
|
|
1646
|
+
},
|
|
1647
|
+
});
|
|
1648
|
+
const error = validateCodexHookSmokeOutput(eventName, run);
|
|
1649
|
+
if (error) {
|
|
1650
|
+
return { ok: false, message: error };
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
return { ok: true, message: 'ok' };
|
|
1655
|
+
} finally {
|
|
1656
|
+
await fs.rm(smokeRoot, { recursive: true, force: true }).catch(() => {});
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
export async function doctorOpenPrdAgentIntegration(projectRoot, options = {}) {
|
|
1661
|
+
const tools = normalizeTools(options.tools);
|
|
1662
|
+
const checks = [];
|
|
1663
|
+
await ensureHarnessState(projectRoot);
|
|
1664
|
+
const manifest = await readInstallManifest(projectRoot);
|
|
1665
|
+
const hookProfile = normalizeHookProfile(options.hookProfile ?? manifest?.hooks?.profile);
|
|
1666
|
+
|
|
1667
|
+
await collectDoctorCheck(projectRoot, checks, 'AGENTS.md', (file) => fileHas(file, 'OPENPRD:AGENTS:START'), 'Missing OpenPrd managed AGENTS block.');
|
|
1668
|
+
await collectDoctorCheck(projectRoot, checks, OPENPRD_HARNESS_MANIFEST, (file) => fileHas(file, '"managedFiles"'), 'Missing OpenPrd install manifest.');
|
|
1669
|
+
await collectDoctorCheck(projectRoot, checks, OPENPRD_HARNESS_HOOK_STATE, (file) => fileHas(file, '"version"'), 'Missing OpenPrd hook state.');
|
|
1670
|
+
await collectDoctorCheck(projectRoot, checks, OPENPRD_HARNESS_EVENTS, (file) => exists(file), 'Missing OpenPrd harness events log.');
|
|
1671
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.openprd/harness/command-catalog.md', 'OpenPrd command catalog');
|
|
1672
|
+
|
|
1673
|
+
if (tools.includes('codex')) {
|
|
1674
|
+
await collectDoctorCheck(projectRoot, checks, '.codex/config.toml', (file) => fileHas(file, 'codex_hooks = true'), 'Codex hooks feature is not enabled.');
|
|
1675
|
+
await collectDoctorCheck(projectRoot, checks, '.codex/hooks.json', (file) => fileHas(file, 'openprd-hook.mjs'), 'Codex hooks.json is missing OpenPrd hooks.');
|
|
1676
|
+
await collectDoctorCheck(projectRoot, checks, '.codex/hooks/openprd-hook.mjs', (file) => fileHas(file, 'OpenPrd harness 上下文'), 'Codex hook runner is missing.');
|
|
1677
|
+
const smoke = await smokeTestCodexHook(projectRoot, { hookProfile });
|
|
1678
|
+
checks.push({ path: '.codex/hooks/openprd-hook.mjs:smoke', ok: smoke.ok, message: smoke.message });
|
|
1679
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.codex/skills/openprd-router/SKILL.md', 'Codex OpenPrd router skill');
|
|
1680
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.codex/skills/openprd-requirement-intake/SKILL.md', 'Codex OpenPrd requirement intake skill');
|
|
1681
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.codex/skills/openprd-harness/SKILL.md', 'Codex OpenPrd harness skill');
|
|
1682
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.codex/skills/openprd-learning-review/SKILL.md', 'Codex OpenPrd learning review skill');
|
|
1683
|
+
if (options.enableUserCodexConfig) {
|
|
1684
|
+
const userConfigPath = cjoin(resolveCodexHome(options), 'config.toml');
|
|
1685
|
+
await collectDoctorCheckAbsolute(checks, displayPath(userConfigPath), userConfigPath, (file) => fileHas(file, 'codex_hooks = true'), 'User Codex config has not enabled hooks.');
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (tools.includes('claude')) {
|
|
1689
|
+
await collectDoctorCheck(projectRoot, checks, 'CLAUDE.md', (file) => fileHas(file, 'OPENPRD:CLAUDE:START'), 'Missing OpenPrd managed CLAUDE block.');
|
|
1690
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.claude/skills/openprd-router/SKILL.md', 'Claude OpenPrd router skill');
|
|
1691
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.claude/skills/openprd-requirement-intake/SKILL.md', 'Claude OpenPrd requirement intake skill');
|
|
1692
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.claude/skills/openprd-harness/SKILL.md', 'Claude OpenPrd harness skill');
|
|
1693
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.claude/skills/openprd-learning-review/SKILL.md', 'Claude OpenPrd learning review skill');
|
|
1694
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.claude/commands/openprd/next.md', 'Claude OpenPrd next command');
|
|
1695
|
+
}
|
|
1696
|
+
if (tools.includes('cursor')) {
|
|
1697
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.cursor/rules/openprd.mdc', 'Cursor OpenPrd rule');
|
|
1698
|
+
await collectGeneratedDoctorCheck(projectRoot, checks, '.cursor/commands/openprd-next.md', 'Cursor OpenPrd next command');
|
|
1699
|
+
}
|
|
1700
|
+
const drift = await computeDriftReport(projectRoot, tools);
|
|
1701
|
+
for (const error of drift.errors) {
|
|
1702
|
+
checks.push({ path: OPENPRD_HARNESS_DRIFT, ok: false, message: error });
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
return {
|
|
1706
|
+
ok: checks.every((check) => check.ok),
|
|
1707
|
+
action: 'doctor',
|
|
1708
|
+
projectRoot,
|
|
1709
|
+
tools,
|
|
1710
|
+
hookProfile,
|
|
1711
|
+
checks,
|
|
1712
|
+
drift,
|
|
1713
|
+
errors: checks.filter((check) => !check.ok).map((check) => (
|
|
1714
|
+
`${check.path}: ${check.message}${check.repairHint ? ` ${check.repairHint}` : ''}`
|
|
1715
|
+
)),
|
|
1716
|
+
};
|
|
1717
|
+
}
|