@harmonyos-arkts/d2h 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +40 -0
  3. package/agents/android-to-hmos-00-orchestrator.md +3 -315
  4. package/agents/d2h.md +53 -38
  5. package/contracts/workflow/artifacts.md +22 -0
  6. package/contracts/workflow/delivery.md +54 -0
  7. package/contracts/workflow/dispatch.md +26 -0
  8. package/contracts/workflow/execution.md +125 -0
  9. package/contracts/workflow/generation.md +40 -0
  10. package/contracts/workflow/orchestration.md +49 -0
  11. package/contracts/workflow/overview.md +58 -0
  12. package/contracts/workflow/plan.md +119 -0
  13. package/contracts/workflow/scope.md +151 -0
  14. package/dist/bin/install-opencode.js +34 -15
  15. package/dist/src/install-codex.js +131 -0
  16. package/dist/src/install-opencode.js +5 -5
  17. package/dist/src/workflow.js +54 -0
  18. package/docs/architecture-plan-guide.zh-CN.md +70 -0
  19. package/docs/incremental-spec-evaluation-design.md +670 -0
  20. package/docs/spec-plan-guide.md +226 -0
  21. package/docs/workflow-decoupling.md +59 -0
  22. package/opencode/interactive-tools/advance-progress-demo.mjs +21 -0
  23. package/opencode/interactive-tools/browser-host.ts +110 -0
  24. package/opencode/interactive-tools/core.ts +242 -0
  25. package/opencode/interactive-tools/demo-workflow.ts +55 -0
  26. package/opencode/interactive-tools/demo.mjs +37 -0
  27. package/opencode/interactive-tools/package-lock.json +1876 -0
  28. package/opencode/interactive-tools/package.json +23 -0
  29. package/opencode/interactive-tools/panel.ts +40 -0
  30. package/opencode/interactive-tools/prepare-demo.mjs +17 -0
  31. package/opencode/interactive-tools/prepare-progress-demo.mjs +13 -0
  32. package/opencode/interactive-tools/progress.ts +118 -0
  33. package/opencode/interactive-tools/server-legacy.mjs +2 -0
  34. package/opencode/interactive-tools/server.mjs +40 -0
  35. package/opencode/interactive-tools/tui.tsx +230 -0
  36. package/package.json +9 -1
  37. package/schemas/native-libs-analysis-report.schema.json +695 -0
  38. package/schemas/native-libs-verify-report.schema.json +566 -0
  39. package/schemas/spec-native-libs-shard.schema.json +110 -0
  40. package/skills/arkts-icon-sizing/SKILL.md +0 -10
  41. package/skills/cpp/d2h-native-library-analyse/SKILL.md +91 -0
  42. package/skills/cpp/d2h-native-library-analyse/scripts/analyse-android-so.py +630 -0
  43. package/skills/cpp/d2h-native-library-build/SKILL.md +403 -0
  44. package/skills/cpp/d2h-native-library-build/references/build-config-mapping.md +180 -0
  45. package/skills/cpp/d2h-native-library-build/references/jni-to-napi-mapping.md +74 -0
  46. package/skills/cpp/d2h-native-library-build/scripts/check-android-so.py +580 -0
  47. package/skills/cpp/d2h-native-library-verify/SKILL.md +111 -0
  48. package/skills/cpp/d2h-native-library-verify/references/demo-build-sign-run.md +67 -0
  49. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/app.json5 +1 -0
  50. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/resources/base/element/string.json +1 -0
  51. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/README.md +25 -0
  52. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/build-profile.json5 +24 -0
  53. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/build-profile.json5 +1 -0
  54. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/hvigorfile.ts +2 -0
  55. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/oh-package.json5 +1 -0
  56. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/entryability/EntryAbility.ets +5 -0
  57. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/Index.ets +3 -0
  58. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/NativeVerifyDemo.ets +276 -0
  59. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/module.json5 +1 -0
  60. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/color.json +1 -0
  61. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/string.json +1 -0
  62. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/profile/main_pages.json +1 -0
  63. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/FFmpegVerify.test.ets +176 -0
  64. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/List.test.ets +2 -0
  65. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/module.json5 +1 -0
  66. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigor/hvigor-config.json5 +1 -0
  67. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigorfile.ts +2 -0
  68. package/skills/cpp/d2h-native-library-verify/references/demo-project-template/oh-package.json5 +9 -0
  69. package/skills/cpp/d2h-native-library-verify/references/demo-template.md +176 -0
  70. package/skills/cpp/d2h-native-library-verify/references/hypium-test-template.md +163 -0
  71. package/skills/cpp/d2h-native-library-verify/scripts/__pycache__/native_common.cpython-313.pyc +0 -0
  72. package/skills/cpp/d2h-native-library-verify/scripts/device-verify.py +304 -0
  73. package/skills/cpp/d2h-native-library-verify/scripts/native_common.py +341 -0
  74. package/skills/cpp/d2h-native-library-verify/scripts/verify-ohos-native.py +1099 -0
  75. package/skills/d2h-app-solution/SKILL.md +198 -0
  76. package/skills/d2h-app-solution/agents/openai.yaml +4 -0
  77. package/skills/d2h-app-solution/references/framework-evidence.md +117 -0
  78. package/skills/d2h-app-solution/references/hierarchy-rules.md +88 -0
  79. package/skills/d2h-app-solution/references/inventory-schema.md +305 -0
  80. package/skills/d2h-app-solution/references/technical-analysis.md +101 -0
  81. package/skills/d2h-app-solution/scripts/scan_repositories.py +350 -0
  82. package/skills/d2h-app-solution/scripts/validate_inventory.py +337 -0
  83. package/skills/d2h-architecture-plan/SKILL.md +35 -0
  84. package/skills/d2h-architecture-plan/references/directory-creation.md +43 -0
  85. package/skills/d2h-architecture-plan/references/hybrid-design.md +84 -0
  86. package/skills/d2h-architecture-plan/references/layout-reference.md +83 -0
  87. package/skills/d2h-architecture-plan/references/plan-contract.md +51 -0
  88. package/skills/d2h-architecture-plan/references/source-review.md +50 -0
  89. package/skills/d2h-cloud-knowledge/SKILL.md +28 -7
  90. package/skills/d2h-cloud-knowledge/kb_query.mjs +8 -0
  91. package/skills/d2h-dashboard/SKILL.md +56 -0
  92. package/skills/d2h-product-family-migration/SKILL.md +103 -0
  93. package/skills/d2h-product-family-migration/agents/openai.yaml +3 -0
  94. package/skills/d2h-product-family-migration/assets/incremental-plan.template.md +55 -0
  95. package/skills/d2h-product-family-migration/references/advanced/behavioral-verification.md +59 -0
  96. package/skills/d2h-product-family-migration/references/advanced/existing-skill-integration.md +30 -0
  97. package/skills/d2h-product-family-migration/references/advanced/packaging-and-verification.md +43 -0
  98. package/skills/d2h-product-family-migration/references/advanced/platform-probes.md +26 -0
  99. package/skills/d2h-product-family-migration/references/advanced/product-line-design.md +40 -0
  100. package/skills/d2h-product-family-migration/references/baseline-and-delta.md +50 -0
  101. package/skills/d2h-product-family-migration/tests/markdown-cases.md +76 -0
  102. package/skills/{d2h-page-develop → d2h-spec-develop}/SKILL.md +143 -132
  103. package/skills/d2h-spec-develop/agents/openai.yaml +7 -0
  104. package/skills/d2h-spec-develop/references/worker-workflow.md +26 -0
  105. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/SKILL.md +24 -13
  106. package/skills/d2h-spec-generate/assets/spec-template.md +109 -0
  107. package/skills/d2h-spec-generate/references/from-plan.md +19 -0
  108. package/skills/d2h-spec-generate/references/spec-content.md +34 -0
  109. package/skills/{d2h-page-spec-generate → d2h-spec-generate}/scripts/build_batch_report.py +35 -84
  110. package/skills/d2h-spec-plan/SKILL.md +38 -0
  111. package/skills/d2h-spec-plan/agents/openai.yaml +4 -0
  112. package/skills/d2h-spec-plan/references/architecture-handoff.md +9 -0
  113. package/skills/d2h-spec-plan/references/plan-contract.md +13 -0
  114. package/skills/d2h-spec-plan/references/planning-method.md +46 -0
  115. package/skills/d2h-spec-plan/references/round-scope-assessment.md +49 -0
  116. package/skills/d2h-spec-plan/references/work-packets.md +61 -0
  117. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +2 -2
  118. package/skills/d2h-utg-ai-generate/references/page-schema.json +2 -2
  119. package/skills/d2h-utg-ai-generate/references/utg-schema.json +2 -2
  120. package/skills/d2h-utg-static-generate/SKILL.md +2 -2
  121. package/skills/managing-d2h-toolchain/environment-requirements.json +1 -1
  122. package/skills/managing-d2h-toolchain/toolchain.json +1 -1
  123. package/skills/preparing-migration-workspace/SKILL.md +32 -2
  124. package/workflow/__pycache__/artifact_bindings.cpython-313.pyc +0 -0
  125. package/workflow/__pycache__/spec_runtime.cpython-313.pyc +0 -0
  126. package/workflow/artifact_bindings.py +224 -0
  127. package/workflow/assets/dashboard.html +88 -0
  128. package/workflow/build_batch_report.py +8 -0
  129. package/workflow/collect_code_metrics.py +75 -0
  130. package/workflow/dashboard_overview.py +109 -0
  131. package/workflow/delivery_view.py +82 -0
  132. package/workflow/feature_plan.py +234 -0
  133. package/workflow/page_batches.py +223 -0
  134. package/workflow/spec_generation.py +120 -0
  135. package/workflow/spec_runtime.py +890 -0
  136. package/workflow/work_packets.py +85 -0
  137. package/skills/d2h-page-develop/agents/openai.yaml +0 -7
  138. package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
  139. package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
  140. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
  141. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
  142. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
  143. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
  144. package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
  145. package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
  146. package/skills/native-library-substitution/SKILL.md +0 -385
  147. package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
  148. package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
  149. package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
  150. package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
  151. package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
@@ -0,0 +1,119 @@
1
+ # v2 计划数据契约
2
+
3
+ 编写或校验 pages.json 时读取;语义拆分与评分方法见 计划生产者的方法说明,派发、证据与修订事件见 [执行协议](execution.md)。
4
+
5
+ 程序路径:`d2h workflow`,Python 3 标准库,无额外运行依赖。它不读取源码推断功能、不自动执行命令或派发 agent,只校验 AI 明确写出的计划与证据。主 Agent 必须独立核验语义和原始工具结果。
6
+
7
+ ## 校验入口
8
+
9
+ 先写计划并评审波次,详细文档可以尚未生成。新计划设置 `spec_generation_policy=required-v1`,生成方按选定版本展开当前成员,详见 [生成契约](generation.md)。只校验规划时运行:
10
+
11
+ ```bash
12
+ d2h workflow validate --planning-only --plan <pages.json> --features <features.json>
13
+ ```
14
+
15
+ 校验结构、来源版本、覆盖关系、依赖与批次预算后,仍需 AI 核验源码及语义。若任务只需计划校验,到此即可;初始化或更新执行状态再读取运行时协议。
16
+
17
+ ## 架构交接
18
+
19
+ 完整迁移按 [产物契约](artifacts.md) 在可选 inputs 中保存 featureAnalysis/architecturePlan/shellReceipt 的绝对路径;feature_source 仍引用版本化功能计划。allocation.owned_paths 沿用确认架构,页面正文保留共享边界与风险。运行时不自动验证这些扩展路径或架构归属,须由主 Agent 核验,不将其写成已有机器门禁。
20
+
21
+ ## 文件与计划示例
22
+
23
+ features.json 新建使用 `d2h-feature-plan/v2`(兼容读取 v1,但不能宣称逐项全量覆盖),由调用方维护。保存内容哈希的方法为 Python `hashlib.sha256(json.dumps(data, sort_keys=True, ensure_ascii=False).encode()).hexdigest()`,即规范化 JSON 内容哈希,不是原始文件字节哈希;source revision 与文件绝对路径都要匹配。
24
+
25
+ features.json v2 的可选 delivery 由上游维护分层映射及目标,跟随 feature revision/hash;不在 pages.json 复制一份选用范围。缺少配置时兼容旧行为,derive.delivery_progress.configured=false;分层累计、范围草稿和 Review 见 [交付与看板](delivery.md)。
26
+
27
+ pages.json 示例(单 Spec,仅示范完整字段,不表示真实应用已实现):
28
+
29
+ ```json
30
+ {
31
+ "schema_version": "d2h-page-plan/v2",
32
+ "revision": 1,
33
+ "feature_source": {"path": "/abs/migration/.migration/feature-planning/features.json", "revision": 1, "sha256": "<canonical-content-hash>"},
34
+ "milestone": "home-skeleton",
35
+ "dispatch_mode": "page-batches",
36
+ "implementation_workers": 1,
37
+ "work_packet_policy": "required-v1",
38
+ "validation_policy": {"max_pending_specs": 3, "max_pending_hours": 4, "max_age_hours": 8},
39
+ "pages": [{"page_id": "login", "package": "page_login", "scope_complete": false}],
40
+ "specs": [{
41
+ "spec_id": "login-basic", "page_id": "login", "feature_refs": ["feature-login"], "milestone": "home-skeleton",
42
+ "scope_refs": [{"feature_id": "feature-login", "scope_id": "login-basic", "checks": ["login-basic/AUTH-01", "login-basic/AUTH-02"]}],
43
+ "scope": ["一个真实登录方式、必要会话和基本失败反馈"],
44
+ "implementation_brief": {
45
+ "behavior_notes": ["合法凭据建立会话并进入首页,失败不建立会话"],
46
+ "invariants": ["登录状态与实际鉴权结果一致"],
47
+ "known_pitfalls": ["页面局部变量不能替代共享会话存储"],
48
+ "open_questions": []
49
+ },
50
+ "evidence_refs": ["source:Login.submit", "user:basic-login"],
51
+ "requires": [],
52
+ "provides": [{"capability": "session.basic", "checks": ["login-basic/AUTH-01", "login-basic/AUTH-02"]}],
53
+ "checks": ["login-basic/AUTH-01", "login-basic/AUTH-02"],
54
+ "ratings": {"milestone": 5, "unlock": 5, "user_value": 5, "risk_reduction": 4, "feedback": 5},
55
+ "effort": {"low": 2, "expected": 4, "high": 8, "confidence": "low", "remaining_elapsed_hours": 6},
56
+ "allocation": {"recommended_workers": 1, "owned_paths": ["entry/src/main/ets/login/"], "rationale": "会话与导航紧密关联,先由单 worker 收敛"},
57
+ "testing": {
58
+ "risk_dimensions": {"impact": 3, "shared": 2, "uncertainty": 1, "dependency": 3},
59
+ "triggers": ["authentication"],
60
+ "immediate": ["login-basic/AUTH-01", "login-basic/AUTH-02"], "batch": [],
61
+ "latest_validation_point": "home-skeleton", "estimated_hours": 1,
62
+ "layers": [
63
+ {"check_id": "login-basic/AUTH-01", "layer": "device", "reason": "真实鉴权和导航"},
64
+ {"check_id": "login-basic/AUTH-02", "layer": "device", "reason": "实际失败不能建立有效会话"}
65
+ ]
66
+ }
67
+ }],
68
+ "rounds": [{
69
+ "round_id": "usable", "order": 1, "kind": "baseline", "milestone": "home-skeleton",
70
+ "goal": "真实登录与必要失败反馈可用", "rationale": "先打通核心入口",
71
+ "goal_checks": ["login-basic/AUTH-01", "login-basic/AUTH-02"]
72
+ }],
73
+ "page_batches": [{
74
+ "batch_id": "login-basic-pass", "page_id": "login", "round_id": "usable",
75
+ "spec_ids": ["login-basic"], "goal": "完成登录基础能力", "rationale": "会话与表单状态共享",
76
+ "max_specs": 3, "max_effort_hours": 6, "opportunistic_hours": 0
77
+ }],
78
+ "dispositions": [],
79
+ "retired_specs": []
80
+ }
81
+ ```
82
+
83
+ - 所有功能需要 spec 覆盖,或显式 disposition `{feature_id, reason}`;included 且阶段已分配、需求清晰的功能不能通过 disposition 隐藏。
84
+ - spec 的 feature_refs 必须指向 included、无需澄清的功能;v2 功能的阶段授权按 scope_refs 对应范围项判定。未分配/延后/排除/待澄清功能留在清单和 disposition,不能被继承优先级强行拉入。
85
+ - 检查 ID 全局唯一,建议 `<spec_id>/<check_id>`。checks 必须由 immediate 和 batch 不重叠地完整分区。provides 的契约检查属于 immediate;高风险全部即时。requires 引用唯一选定提供者,先校验用户顺序和能力依赖的组合 DAG。
86
+ - `requested_before` 为功能级约束:前一功能的相关 Spec 完整验收后,才允许后一功能。若用户只希望基本能力提前,应先在上游准确缩小功能范围或拆分功能,而非默默弱化该约束。
87
+ - 只执行当前 milestone 及其必要早期依赖,页面模式还限制在当前未交付轮次。有效优先级为自身与未完成目标继承值中的最高值,inherited_from 返回 spec_id;用户优先级不变。先比较有效优先级、剩余关键路径与共享解锁,同级优先同页连续开发,再比较收益/成本及稳定 batch_id。主 Agent 阅读 ready_batches 后选择,不把脚本候选当源码语义判决。
88
+ - 日期采用带时区 ISO8601。测试积压数量、估计小时、等待年龄任一达到上限停止新 start。独立工作可在未触发背压时继续,消费者始终受即时能力约束;修复和验证不受新增实现背压阻止。
89
+
90
+ ## 轮次与页面批次
91
+
92
+ 轮次范围评估由计划生产方负责,按 调用方的轮次范围评估 记录目标、范围、最小前置、暂缓理由、成本风险及验收条件。复用已有 rounds.goal/rationale/goal_checks、page_batches、Spec.scope_refs 与页面 spec.md;不新增独立轮次真源,也不修改旧计划 schema。
93
+
94
+ 新增计划显式写 `dispatch_mode=page-batches`;省略时按既有 single-spec 行为兼容,不隐式改在途任务。rounds 与 page_batches 只能在页面模式使用,转换须保存旧基线、释放在途任务及批次后 reconcile。
95
+
96
+ - 里程碑来自上游,round 是其内部的实现轮次,具有独立 goal 和目标路径 goal_checks,不能替代或改写用户范围。一个里程碑可以包含多轮,一个页面可在多轮出现。每个里程碑第一轮且仅一轮为 baseline;后续为任意多轮 incremental,completion 若存在只能在该里程碑最后。轮次 order 按数组连续且不能倒置里程碑顺序。
97
+ - page_batch 恰好属于一轮和一个主页面;spec_ids 不重复且全部 Spec 恰好覆盖一次。各成员的 page_id 和 milestone 必须匹配;max_specs/max_effort_hours 限制完整计划批次,超限须拆分。rationale 记录真实状态、代码或测试准备复用依据,不能仅因名称相似合并。
98
+ - 当前里程碑引用早期轮次时,补齐该轮完整成员、同里程碑更早轮次及其能力前置,不能只纳入半个轮次导致目标无法验收。这些成员须已由用户授权并显式列在计划中。
99
+ - 内部依赖保留并逐项核验。不能依赖更晚轮次,也不能形成“页面 A 第一项→页面 B→页面 A 第二项”的批次收缩环;此时拆成不同访问批次。归拢不会把未验证能力变成 ready。
100
+ - opportunistic_hours 为低优顺带项预算,默认建议 0。派发时先选最高有效优先级成员及其必要前置;仅当较低优成员总成本在预算内、且没有其他就绪页面更高优任务等待时一起领取。否则低优项留在原批次后续派发,不丢失需求。实际窗口的成员由 derive 固定建议,start_batch 必须完全匹配该建议,且只能选择就绪批次的最高有效优先级层。若外部更高优任务在领取后恢复,开始顺带项前应暂停并让出窗口。
101
+ - active_batch 是独占的实际窗口;同一 worker 在其中逐 Spec start,实现和测试。一次领取多项不表示并发多个 worker。窗口外任务禁止 start,跨窗口 repair 也需先暂停现有窗口。成员或外部依赖中途受阻时显式 pause、pause_batch 保存进度,恢复后仅建议剩余成员。
102
+ - derive 返回 current_round、rounds、page_batches、ready_batches、active_batch、pages、page_dispatch_metrics。ready 仅表示已领取窗口内当前能启动的 Spec;ready_specs 是当前轮次的观察值,不能绕过 start_batch 直接派发。无活动窗口时 ready 为空。
103
+ - pages[].scope_complete 是 AI 核对全部约定页面范围的声明,为 true 时必须有 scope_evidence_refs(非空源码/清单范围依据);它本身不是实现完成状态。输出 pages[page_id].baseline_verified 代表该页第一次访问范围在当前候选已验收,complete 还要求全量范围声明及该页所有后续 Spec 当前验收。page_pass 为局部批次序号,不能与全局 round.order 混淆。
104
+ - round_accept 必须逐项 accepted 后执行目标路径。历史 delivered 保留经过核验的目标签名和不可变原始证据;current_accepted 还需当前候选全部成员及目标证据有效。候选变化不抹去历史交付,也不把新轮开发无条件退回首轮;但当前能力与验收不会复用旧 HAP PASS。显式 failed/invalidate 撤销受影响轮次和依赖消费者的可用历史证明,原事件仍保留。
105
+ - 最终里程碑必须所有相关轮次 current_accepted 且完整 regression 通过。同一真实最终回归可以分别映射各轮目标和 Spec 检查,不宣称重复引用就是多次执行。HAP 和报告使用不可变证据路径,不能覆盖导致历史证明丢失。
106
+
107
+ ## 逐项范围映射与完成语义
108
+
109
+ 新功能清单使用 d2h-feature-plan/v2。每个 Spec 的 scope_refs 为非空数组,每项包含 feature_id、scope_id 和非空 checks(该 Spec 实际验收检查的子集)。每个 feature_ref 和每项 Spec check 均须被映射;一个已分配且授权 included 的范围项恰好由一个 Spec 负责。若一项需求必须由多个独立 Spec 分担,先在上游拆成可独立验收的范围项。依赖复用用 requires/provides,不重复认领同一范围。
110
+
111
+ 功能可跨里程碑;deferred/excluded/unresolved/unassigned 项不强制提前生成 Spec,但始终留在 feature_progress 中。缺失、重复认领、越阶段或越 checks 引用均拒绝。feature_progress.complete 仅当所有未排除项在当前候选验收通过;scope_coverage_accepted 是全清单范围门禁,不等于全应用最终回归。旧 v1 输出 scope_tracking=false、scope_coverage_accepted=false,不能凭 feature_refs 宣称全量完成。
112
+
113
+ 同页 spec.md 持续维护“范围项 → 轮次 → Spec → checks”表。基础 Spec 完成后保持 ID、范围和历史;完善功能新增增量 Spec,复用页面与已验证能力。仅需求变更才修订原 Spec,回归故障则 invalidate/repair。reconcile 对新增无关范围保留原 Spec;修改其实际范围、断言或映射会使受影响验收失效。页面 complete 还要求该页所引用功能的范围全部解决;跨页功能采取保守关联,不能只凭当前页面的已生成 Spec 声称完整。
114
+
115
+ ## 实施工作包
116
+
117
+ 页面文档按 完整行为规格及调用方模板 生成,保留页面共享上下文及独立 spec_id 章节。page_batches 是同轮同页的组织与派发分组,不合并成员验收身份;无需新增 generation_groups 或改变现有 schema。
118
+
119
+ 规划不编写详细 spec.md。新计划显式启用 work_packet_policy=required-v1 与 spec_generation_policy=required-v1;前者约束实施输入,后者对未生成/失效正文阻止派发。implementation_brief、testing.layers、shared_contracts 的字段与开工条件见 [派发契约](dispatch.md)。旧计划不隐式开启;升级须释放在途任务并 reconcile,策略变化会重新核验实施范围。接口回执只在执行账本维护。
@@ -0,0 +1,151 @@
1
+ # 功能规划与下游交接
2
+
3
+ ## 输入与处理
4
+
5
+ 默认输出 `<migration_root>/.migration/feature-planning/features.json`;无迁移根时用用户指定目录。原始 `inventory.json`(兼容旧 feature-analysis.json)保留在 `.migration/feature-analysis/`,不复制成另一份可修改分析真源。原始分析只读,来源结构和字节绑定由公共工具核验;不调用分析生产者的私有校验程序。
6
+
7
+ 1. 读取用户提供的输入和已有计划。Facts 的 index、unresolved 和 UTG 是范围与证据线索;需要确认可见行为时继续读取对应源码,不能把抽取结果当作运行时验证。
8
+ 2. 按稳定引用识别相同功能;名称相似只作为候选。不同入口可以共享能力,但保留路径差异。若一个盘点条目对应多个规划功能或反之,通过 source_refs 显式表达映射。
9
+ 3. 将用户新增需求、范围修改、里程碑、优先级、顺序分别记录。优先级为 must/normal/later,默认 normal。交付优先采用用户明确选中的范围,恢复时保留仍有效的既有用户选择;无明确选择时按默认核心功能规划。AI 为核心范围及必要依赖安排阶段,未排期或待澄清项保持 unassigned,不伪装成用户已指定阶段。
10
+ 4. 保持 feature_id 稳定。重命名、排序不改变 ID;拆分或合并分配新 ID,通过 derived_from 保留来源。延后不删除功能,排除保留原因。
11
+ 5. 输出 features.json,运行校验,并交付简要的功能/阶段/优先级/顺序/未决项摘要。计划模式不要求 CSV、XLSX 或额外分析文件。
12
+
13
+ 用户要求“登录基本可用,首页优先”时,将基本登录和其余约定范围分别登记为稳定 scope_items;其余项明确延后;首页记录用户优先级。向下游传达最小前置切片建议,不在此创建 session.basic 或伪造登录已通过。下游负责推导和验证实际能力依赖。
14
+
15
+ ## 输出契约:d2h-feature-plan/v2
16
+
17
+ ```json
18
+ {
19
+ "schema": "d2h-feature-plan/v2",
20
+ "revision": 1,
21
+ "sources": [
22
+ {
23
+ "source_id": "user-1",
24
+ "kind": "user",
25
+ "locator": "本轮用户需求",
26
+ "revision": "message-1"
27
+ }
28
+ ],
29
+ "milestones": [
30
+ {
31
+ "milestone_id": "home-skeleton",
32
+ "title": "首页雏形",
33
+ "acceptance": [
34
+ "真实登录后可使用首页主导航"
35
+ ]
36
+ }
37
+ ],
38
+ "features": [
39
+ {
40
+ "feature_id": "feature-login",
41
+ "title": "登录",
42
+ "origin": "user",
43
+ "source_refs": [
44
+ {
45
+ "source_id": "user-1",
46
+ "ref": "登录基本可用"
47
+ }
48
+ ],
49
+ "acceptance": [
50
+ "合法凭据登录后进入首页;失败不建立有效会话"
51
+ ],
52
+ "milestone": "home-skeleton",
53
+ "user_priority": "must",
54
+ "scope_status": "included",
55
+ "reason": "用户要求先打通基本登录",
56
+ "requested_before": [],
57
+ "derived_from": [],
58
+ "needs_clarification": false,
59
+ "scope_items": [
60
+ {
61
+ "scope_id": "login-basic",
62
+ "description": "一个真实登录方式、必要会话与基本失败反馈",
63
+ "status": "included",
64
+ "milestone": "home-skeleton",
65
+ "acceptance": [
66
+ "合法凭据登录后进入首页;失败不建立有效会话"
67
+ ]
68
+ },
69
+ {
70
+ "scope_id": "login-other-methods",
71
+ "description": "其他约定登录方式",
72
+ "status": "deferred",
73
+ "milestone": "unassigned",
74
+ "reason": "基础路径完成后再分配轮次",
75
+ "acceptance": [
76
+ "约定登录方式能建立真实会话"
77
+ ]
78
+ }
79
+ ]
80
+ }
81
+ ],
82
+ "changes": [
83
+ {
84
+ "revision": 1,
85
+ "actor": "user",
86
+ "feature_ids": [
87
+ "feature-login"
88
+ ],
89
+ "summary": "新增基本登录需求"
90
+ }
91
+ ],
92
+ "open_questions": []
93
+ }
94
+ ```
95
+
96
+ - sources.kind:facts / utg / inventory / source / user;locator 指向文件、目录或用户输入标识,revision 记录真实产物版本、提交或消息标识,不填推测值。
97
+ - source_refs.ref 为 JSON Pointer、稳定事实 ID、源码符号或用户原始要求。source_id 必须存在;引用存在性与是否支持结论由 AI 阅读复核,校验脚本不证明语义正确。
98
+ - origin:extracted / user / merged。extracted 至少有非 user 来源;user 至少有 user 来源;merged 两者均需。用户仅调整一个源码功能的优先级时,可保留 extracted,变更记入 changes,不伪造事实变化。
99
+ - scope_status:included / deferred / excluded。deferred 与 excluded 必须有 reason;延后功能仍保留在全量清单。scope_items 是范围的唯一真源,不维护 required_scope/deferred_scope 的平行副本。
100
+ - requested_before 表示显式用户顺序约束,不是源码能力依赖;禁止自身引用、未知 ID、环及跨里程碑约束。跨阶段先后通过 milestones 数组顺序表达;需要调整阶段时记录变更。
101
+ - features 数组只是稳定展示顺序。用户明确排序时在同阶段按顺序建立相邻 requested_before,避免下游误把默认数组顺序当强约束。
102
+ - milestone 必须引用已声明阶段或 unassigned;scope_items[].milestone 同样适用。未分配阶段的需求不得自动进入当前执行批次。
103
+ - derived_from 可引用历史 ID,保留拆分合并关系;changes 追加记录,不覆盖既有历史。每次实际修改 revision 增一,无变化保持原 revision。
104
+ - 本契约不包含实现完成状态、系统评分、worker_count 或测试通过字段,避免成为下游第二份执行账本。
105
+
106
+ 校验:
107
+
108
+ ```bash
109
+ python3 <skill-dir>/scripts/validate_feature_plan.py /abs/path/features.json
110
+ ```
111
+
112
+ 更新已有计划时追加 `--previous /abs/path/previous-features.json`,校验版本递增、变更历史前缀及旧 ID 保留。拆分合并后的旧项保留并标 excluded,reason 写明被替代关系。
113
+
114
+ ## 下游交接与冲突
115
+
116
+ 交接绝对文件路径、schema 和 revision。下游保存消费的版本及 feature_refs,读取 scope_items、用户优先级和顺序约束,再推导增量 Spec。自动评分只能在用户约束内排序。
117
+
118
+ 若依赖与用户顺序冲突,下游反馈具体功能、证据、最小前置能力与建议;本模块根据授权修改计划并保留记录,下游不能静默改写。执行中固定派发版本,计划更新不得自动改写正在进行的任务。
119
+
120
+ 计划消费方读取 features.json 的路径、revision 和内容哈希,生成 v2 增量计划,由调用方派发实施能力。用户优先级不可机械换算成旧 page_score;旧 v1 计划需显式迁移并保留历史。当前实现限单实现 worker,能力即时门禁及验证队列由下游负责,不在本模块维护执行状态。
121
+
122
+ ## 稳定范围项与渐进完善
123
+
124
+ - scope_items[].scope_id 在所属 feature 内唯一且稳定;重排、延期和改名不改变 ID。description、acceptance、status、milestone 描述需求与处置,不记录实现状态。status 为 included/deferred/excluded/unresolved,非 included 必须有 reason。
125
+ - 功能 milestone 是默认规划归属;v2 中每项 scope_items.milestone 决定其实际交付阶段,同一功能可跨阶段。仅状态 included、阶段已分配且功能已授权、无需澄清的项进入 Spec。
126
+ - 不能为赶基础路径删除已知范围。其余范围保留为 deferred 或 unassigned;未解决差异用 unresolved。重新排期时保持 scope_id,将对应项改为 included 并分配阶段,由下游新增增量 Spec。
127
+ - 拆分范围时给新项分配 ID,保留旧项并以 excluded/reason 说明替代关系;旧范围 ID 不得删除。AI 核对需求是否穷尽、描述和断言是否相符,脚本只保证结构与映射完整。
128
+ - 旧 v1 可继续读取,但不能宣称逐项全量覆盖。升级 v2 时先归档旧清单、计划和执行账本;由 AI 将 required_scope/deferred_scope 显式拆成 scope_items,保留 feature_id,revision 递增并记录变更,用 --previous 校验;下游同步 scope_refs 并 reconcile,旧验收不自动变成新范围的证据。
129
+
130
+ 范围项描述可识别的需求,不按按钮或每个校验条件机械拆分。多个紧密关联范围项可由下游一个 Spec 的多个 scope_refs 覆盖;上游保留需求完整性,下游依据交付、依赖和风险决定 Spec 粒度,未排期范围不提前展开实现细节。
131
+
132
+ ## 分层交付与看板草稿
133
+
134
+ v2 可选 delivery;进入迁移交付规划时,用户明确选择优先,无明确选择则默认核心(target=core)。由 AI 按实际路径提出完整范围映射,将本次范围及必要依赖写为 included 并安排阶段,其他范围延期并保留原因;默认核心在 delivery.reason 与 changes(actor=ai)中说明,不伪造用户选择,不改 user_priority。仅分析/导出报告不自动启动交付规划。示例结构:
135
+
136
+ ```json
137
+ {"delivery":{"target":"core","customized":false,"reason":"用户选择核心实现,其他范围后续排期","scope_layers":[{"feature_id":"feature-login","scope_id":"login-basic","layer":"mvp"},{"feature_id":"feature-login","scope_id":"login-other-methods","layer":"full"}]}}
138
+ ```
139
+
140
+ 交付目标与最低交付层独立:用户明确选择全量交付时 target=full,但每项 layer 仍按真实路径判定,不能批量设为 full。全量盘点只决定分析覆盖,不等于选择全量交付;进入迁移交付规划时仍按用户选择或默认核心确定目标。先识别项目最小可用闭环及必要共享能力,再区分核心增强和全量长尾;不得仅凭 P0/P1/P2、user_priority、页面次序或名称自动分层。对已有多功能应用,导出前核对 MVP 累计预设非空、核心预设包含 MVP 且确有可解释范围;小应用核心与 MVP 可以相同,不为凑层数创造功能。分层尚无依据时保留未知并说明,不造一个全 full 的已完成配置。
141
+
142
+ 分别检查三层累计数量、范围样例与必要依赖:依赖应在同层或更早层;无法裁决的跨层依赖保持未决,不擅自扩大授权。当前 target、scope_items 的纳入/延期状态及执行窗口不因生成最低层映射而自动改变。历史已配置错误时显式修订,保存基线及 revision/changes,再交下游 reconcile。
143
+
144
+ target/layer 为 mvp/core/full;customized 为布尔值,reason 非空。所有未排除范围必须恰好映射一层,包含延期、未决和未分配项;排除旧项可保留原映射。MVP⊆核心⊆全量按最低层累计。customized=false 时,选用并已授权/分配的范围须与目标预设累计集合完全相同;包含未决项的全量预设不能自动授权,先澄清或以 customized=true 明确保留待解决项。customized=true 时按 scope_items 实际授权,预设名称只保留为调整来源。
145
+
146
+ 勾选/取消及优先级调整形成草稿,取消默认延期,明确排除另写依据。预设不覆盖手动优先级与 requested_before。修改 delivery 也递增 F revision 并追加 changes,旧清单保持兼容且不自动产生配置。首次功能基线归档至同目录 baseline-features.json,后续不覆盖。
147
+
148
+ 看板草稿不是保存成功的计划。接收 d2h-scope-change-request/v1 时先核对 feature/plan hash、候选与事件序号,再解释范围、依赖及优先级;跨范围最小前置与实际业务取舍交用户,不静默扩大。先更新并校验上游,再交规划能力返回修订结果,由调用方 reconcile;不能把用户草稿中的完成状态当成证据。详见 [交付与看板](delivery.md)。
149
+
150
+
151
+ 使用最小化/核心/全量预设(含无明确选择时的默认核心)时,交接前运行 `validate_feature_plan.py <features.json> --require-delivery`。该检查要求显式配置且三个累计预设非空,不能用“全部 full”配置充当完成分层;允许核心与 MVP 相同。不覆盖用户明确选择;结构校验不证明路径语义或依赖闭环,仍由 AI 核对。依据不足时先补齐核心范围与必要依赖,不让看板或运行时凭缺省字段自动猜选。
@@ -1,4 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
2
10
  import { fileURLToPath } from 'node:url';
3
11
  import path from 'node:path';
4
12
  import os from 'node:os';
@@ -6,14 +14,17 @@ import { inspectInstallation, installOpenCode } from '../src/install-opencode.js
6
14
  import { inspectToolchain, installToolchain, loadToolchain } from '../src/toolchain.js';
7
15
  import fs from 'node:fs';
8
16
  import { inspectVSCodeInstallation, installVSCode } from '../src/install-vscode.js';
9
- const help = `Install this package's skills and agents into OpenCode or DevEco Code.
17
+ import { codexConfigRoot, inspectCodexInstallation, installCodex } from '../src/install-codex.js';
18
+ import { runWorkflow } from '../src/workflow.js';
19
+ const help = `Install this package's skills and agents into OpenCode, DevEco Code or Codex.
10
20
 
11
21
  Usage: d2h [options]
12
22
 
13
23
  d2h install Install compatible CLIs and agent/skill definitions
14
24
  d2h upgrade Reapply this package's tested toolchain and definitions
15
25
  d2h upgrade --check Preview version and installation status only
16
- d2h doctor [--agent opencode|deveco|vscode] [--config-dir <path>]
26
+ d2h workflow Validate artifacts, record execution and generate reports
27
+ d2h doctor [--agent opencode|deveco|vscode|codex] [--config-dir <path>]
17
28
  Check definitions, host environment and CLI capabilities
18
29
 
19
30
  --check-env Check host tools and write reports without installing skills
@@ -21,11 +32,12 @@ Usage: d2h [options]
21
32
  Use d2h --check-env --help for check options
22
33
 
23
34
  --source-dir <path> Source root (defaults to this package)
24
- --agent <name> opencode (default), deveco or vscode
35
+ --agent <name> opencode (default), deveco, vscode or codex
25
36
  --extension-dir <path> VS Code HarmonyOS Dev Assistant extension root
26
37
  --config-dir <path> Override the selected agent's config root
27
38
  OPENCODE_CONFIG_DIR / DEVECO_CONFIG_DIR, or
28
39
  $XDG_CONFIG_HOME/<agent>, or ~/.config/<agent>
40
+ Codex: ~/.agents (skills under skills/; --config-dir overrides)
29
41
  --link Create absolute symbolic links (development only)
30
42
  --force Replace conflicting definitions and managed CLI npm links
31
43
  --remote Use opencode-metrics-plugin instead of the default lite plugin
@@ -40,7 +52,10 @@ With npx, prefer the default copy mode: its cache can be removed.
40
52
  try {
41
53
  const args = process.argv.slice(2);
42
54
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
43
- if (args[0] === 'doctor') {
55
+ if (args[0] === 'workflow') {
56
+ process.exitCode = runWorkflow(packageRoot, args.slice(1));
57
+ }
58
+ else if (args[0] === 'doctor') {
44
59
  let agent = 'opencode';
45
60
  let configDir;
46
61
  let extensionDir;
@@ -60,10 +75,10 @@ try {
60
75
  else
61
76
  throw new Error(`unknown doctor option: ${arg}`);
62
77
  }
63
- if (!['opencode', 'deveco', 'vscode'].includes(agent))
78
+ if (!['opencode', 'deveco', 'vscode', 'codex'].includes(agent))
64
79
  throw new Error(`unsupported agent: ${agent}`);
65
- configDir ??= process.env[agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), agent);
66
- const installation = agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : inspectInstallation(configDir);
80
+ configDir ??= agent === 'codex' ? codexConfigRoot() : process.env[agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), agent);
81
+ const installation = agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : agent === 'codex' ? inspectCodexInstallation(configDir) : inspectInstallation(configDir);
67
82
  const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version;
68
83
  const definitionsMatch = installation.status === 'passed' && installation.packageVersion === packageVersion &&
69
84
  (agent === 'vscode' || ('agent' in installation && installation.agent === agent));
@@ -72,7 +87,7 @@ try {
72
87
  for (const item of results)
73
88
  console.log(`${item.status}: ${item.package} expected ${item.expectedVersion}, installed ${item.installedVersion ?? 'missing'}; capabilities ${JSON.stringify(item.capabilities)}`);
74
89
  const checkerUrl = new URL('../../scripts/check-environment.mjs', import.meta.url);
75
- const { main } = await import(checkerUrl.href);
90
+ const { main } = await import(__rewriteRelativeImportExtension(checkerUrl.href));
76
91
  const environmentExit = await main([]);
77
92
  if (environmentExit !== 0 || !definitionsMatch || results.some(item => item.status !== 'passed'))
78
93
  process.exitCode = 1;
@@ -81,7 +96,7 @@ try {
81
96
  if (args.filter(arg => arg === '--check-env').length !== 1)
82
97
  throw new Error('--check-env may only be specified once');
83
98
  const checkerUrl = new URL('../../scripts/check-environment.mjs', import.meta.url);
84
- const { main } = await import(checkerUrl.href);
99
+ const { main } = await import(__rewriteRelativeImportExtension(checkerUrl.href));
85
100
  process.exitCode = await main(args.filter(arg => arg !== '--check-env'));
86
101
  }
87
102
  else {
@@ -127,16 +142,18 @@ try {
127
142
  throw new Error(`unknown option: ${arg}`);
128
143
  }
129
144
  }
130
- if (!options.agent || !['opencode', 'deveco', 'vscode'].includes(options.agent)) {
131
- throw new Error(`unsupported agent: ${options.agent}; choose opencode, deveco or vscode`);
145
+ if (!options.agent || !['opencode', 'deveco', 'vscode', 'codex'].includes(options.agent)) {
146
+ throw new Error(`unsupported agent: ${options.agent}; choose opencode, deveco, vscode or codex`);
132
147
  }
148
+ if (options.agent === 'codex' && remote)
149
+ throw new Error('--remote is not supported by Codex');
133
150
  if (options.agent !== 'vscode' && extensionDir)
134
151
  throw new Error('--extension-dir requires --agent vscode');
135
152
  if (options.agent === 'vscode' && options.configDir)
136
153
  throw new Error('--config-dir is not used for VS Code; use --extension-dir');
137
- options.configDir ??= process.env[options.agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), options.agent);
154
+ options.configDir ??= options.agent === 'codex' ? codexConfigRoot() : process.env[options.agent === 'deveco' ? 'DEVECO_CONFIG_DIR' : 'OPENCODE_CONFIG_DIR'] || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), options.agent);
138
155
  if (checkOnly) {
139
- const installation = options.agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : inspectInstallation(options.configDir);
156
+ const installation = options.agent === 'vscode' ? inspectVSCodeInstallation(extensionDir) : options.agent === 'codex' ? inspectCodexInstallation(options.configDir) : inspectInstallation(options.configDir);
140
157
  console.log(`definitions ${installation.status}: ${installation.summary}`);
141
158
  for (const item of inspectToolchain(packageRoot))
142
159
  console.log(`${item.status}: ${item.package} expected ${item.expectedVersion}, installed ${item.installedVersion ?? 'missing'}`);
@@ -148,18 +165,20 @@ try {
148
165
  if (!checkOnly) {
149
166
  if (options.agent === 'vscode')
150
167
  installVSCode({ sourceDir: options.sourceDir, extensionDir, remote, dryRun: options.dryRun });
168
+ else if (options.agent === 'codex')
169
+ installCodex({ ...options, remote });
151
170
  else
152
171
  installOpenCode({ ...options, remote });
153
172
  if (!options.dryRun) {
154
173
  const target = options.agent === 'vscode' ? 'VS Code HarmonyOS Dev Assistant' :
155
- options.agent === 'deveco' ? 'DevEco Code' : 'OpenCode';
174
+ options.agent === 'deveco' ? 'DevEco Code' : options.agent === 'codex' ? 'Codex' : 'OpenCode';
156
175
  const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version;
157
176
  console.log('\n============================================================');
158
177
  console.log(`[D2H] Installation successful / 安装成功`);
159
178
  console.log(`Target / 目标: ${target}`);
160
179
  console.log(`Version / 版本: ${packageVersion}`);
161
180
  console.log('Agent: d2h');
162
- console.log(`Next / 下一步: restart ${target}, then select the d2h Agent.`);
181
+ console.log(options.agent === 'codex' ? 'Next / 下一步: start a new Codex conversation and invoke $d2h.' : `Next / 下一步: restart ${target}, then select the d2h Agent.`);
163
182
  console.log('============================================================');
164
183
  }
165
184
  }
@@ -0,0 +1,131 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { stat, digest, sameTree, contains, resolvedLocation } from './install-opencode.js';
5
+ export function codexConfigRoot() {
6
+ return path.join(os.homedir(), '.agents');
7
+ }
8
+ // Keep the orchestration prompt as an explicitly invokable skill: custom agents
9
+ // are subagents in Codex, whereas D2H needs to drive the main conversation.
10
+ function definitions(sourceRoot) {
11
+ const skills = path.join(sourceRoot, 'skills');
12
+ const entries = fs.readdirSync(skills).sort()
13
+ .filter(name => fs.existsSync(path.join(skills, name, 'SKILL.md')))
14
+ .map(name => ({ name, source: path.join(skills, name) }));
15
+ if (!entries.length)
16
+ throw new Error(`no skills found under ${skills}`);
17
+ if (entries.some(entry => entry.name === 'd2h'))
18
+ throw new Error('reserved Codex orchestration skill: d2h');
19
+ const prompt = fs.readFileSync(path.join(sourceRoot, 'agents', 'd2h.md'), 'utf8');
20
+ if (!/^---\r?\n[\s\S]*?\r?\n---\r?\n/.test(prompt))
21
+ throw new Error('d2h agent front matter missing');
22
+ return [...entries, { name: 'd2h', content: prompt }];
23
+ }
24
+ export function inspectCodexInstallation(configDir = codexConfigRoot()) {
25
+ const root = path.resolve(configDir);
26
+ const receipt = path.join(root, '.d2h-codex-installation.json');
27
+ if (!fs.existsSync(receipt))
28
+ return { status: 'missing', summary: `installation record missing: ${receipt}` };
29
+ const record = JSON.parse(fs.readFileSync(receipt, 'utf8'));
30
+ if (record.schemaVersion !== 1 || record.agent !== 'codex' || !Array.isArray(record.entries) || !record.entries.length) {
31
+ throw new Error(`invalid installation record: ${receipt}`);
32
+ }
33
+ const changed = record.entries.filter(entry => {
34
+ if (!/^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*$/.test(entry.name))
35
+ throw new Error(`invalid installation entry: ${entry.name}`);
36
+ const target = path.join(root, 'skills', entry.name);
37
+ return !stat(target) || digest(target) !== entry.digest;
38
+ });
39
+ return { status: changed.length ? 'failed' : 'passed', agent: 'codex', packageVersion: record.packageVersion,
40
+ summary: changed.length ? `changed or missing: ${changed.map(entry => entry.name).join(', ')}` : `${record.entries.length} installed skills verified` };
41
+ }
42
+ export function installCodex({ sourceDir, configDir = codexConfigRoot(), link = false, force = false, dryRun = false, remote = false, log = console.log }) {
43
+ if (remote)
44
+ throw new Error('--remote metrics plugins are not supported by Codex');
45
+ if (!configDir || ['.', '..'].includes(configDir))
46
+ throw new Error(`unsafe Codex config directory: ${configDir}`);
47
+ const sourceRoot = fs.realpathSync(path.resolve(sourceDir));
48
+ const root = resolvedLocation(path.resolve(configDir));
49
+ if (root === path.parse(root).root || root === fs.realpathSync(os.homedir()) ||
50
+ contains(sourceRoot, root) || contains(root, sourceRoot))
51
+ throw new Error(`unsafe Codex config directory: ${configDir}`);
52
+ const skillsRoot = path.join(root, 'skills');
53
+ if (stat(skillsRoot)?.isSymbolicLink())
54
+ throw new Error(`refusing linked installation directory: ${skillsRoot}`);
55
+ const receipt = path.join(root, '.d2h-codex-installation.json');
56
+ if (stat(receipt)?.isSymbolicLink())
57
+ throw new Error(`refusing linked installation record: ${receipt}`);
58
+ const entries = definitions(sourceRoot).map(entry => {
59
+ const target = path.join(skillsRoot, entry.name);
60
+ const existing = stat(target);
61
+ const unchanged = entry.source ? (link && existing?.isSymbolicLink()
62
+ ? path.resolve(path.dirname(target), fs.readlinkSync(target)) === entry.source
63
+ : !link && sameTree(entry.source, target))
64
+ : existing?.isDirectory() && !existing.isSymbolicLink() &&
65
+ fs.readdirSync(target).length === 1 && fs.existsSync(path.join(target, 'SKILL.md')) &&
66
+ fs.readFileSync(path.join(target, 'SKILL.md'), 'utf8') === entry.content;
67
+ if (!unchanged && existing && !force)
68
+ throw new Error(`skill '${entry.name}' already exists at ${target}; rerun with --force`);
69
+ return { ...entry, target, unchanged };
70
+ });
71
+ if (dryRun) {
72
+ for (const entry of entries)
73
+ log(`would install Codex skill: ${entry.name} -> ${entry.target}`);
74
+ return { skills: entries.length, configRoot: root };
75
+ }
76
+ fs.mkdirSync(skillsRoot, { recursive: true });
77
+ if (!contains(root, fs.realpathSync(skillsRoot)))
78
+ throw new Error(`installation escaped config directory: ${skillsRoot}`);
79
+ for (const entry of entries) {
80
+ if (entry.unchanged) {
81
+ log(`unchanged Codex skill: ${entry.name}`);
82
+ continue;
83
+ }
84
+ const staging = fs.mkdtempSync(path.join(skillsRoot, '.d2h-install-'));
85
+ const staged = path.join(staging, 'new');
86
+ const backup = path.join(staging, 'backup');
87
+ try {
88
+ if (entry.source) {
89
+ if (link)
90
+ fs.symlinkSync(entry.source, staged, process.platform === 'win32' ? 'junction' : 'dir');
91
+ else
92
+ fs.cpSync(entry.source, staged, { recursive: true });
93
+ }
94
+ else {
95
+ fs.mkdirSync(staged);
96
+ fs.writeFileSync(path.join(staged, 'SKILL.md'), entry.content);
97
+ }
98
+ if (stat(entry.target))
99
+ fs.renameSync(entry.target, backup);
100
+ try {
101
+ fs.renameSync(staged, entry.target);
102
+ }
103
+ catch (error) {
104
+ if (stat(backup))
105
+ fs.renameSync(backup, entry.target);
106
+ throw error;
107
+ }
108
+ }
109
+ finally {
110
+ if (stat(backup) && !stat(entry.target))
111
+ log(`installation backup retained: ${backup}`);
112
+ else
113
+ fs.rmSync(staging, { recursive: true, force: true });
114
+ }
115
+ log(`installed Codex skill: ${entry.target}`);
116
+ }
117
+ const packageFile = path.join(sourceRoot, 'package.json');
118
+ const packageVersion = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, 'utf8')).version : 'source';
119
+ const temporary = `${receipt}.${process.pid}.tmp`;
120
+ try {
121
+ fs.writeFileSync(temporary, JSON.stringify({ schemaVersion: 1, agent: 'codex', packageVersion,
122
+ entries: entries.map(entry => ({ name: entry.name, digest: digest(entry.target) })) }, null, 2) + '\n');
123
+ fs.renameSync(temporary, receipt);
124
+ }
125
+ finally {
126
+ if (fs.existsSync(temporary))
127
+ fs.rmSync(temporary);
128
+ }
129
+ log(`Codex installation complete: ${entries.length} skill(s). Start a new conversation and invoke $d2h.`);
130
+ return { skills: entries.length, configRoot: root };
131
+ }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import os from 'node:os';
4
4
  import crypto from 'node:crypto';
5
5
  import { updateMetricsPlugin } from './metrics-plugin.js';
6
- function stat(file) {
6
+ export function stat(file) {
7
7
  try {
8
8
  return fs.lstatSync(file);
9
9
  }
@@ -13,7 +13,7 @@ function stat(file) {
13
13
  throw error;
14
14
  }
15
15
  }
16
- function sameTree(source, destination) {
16
+ export function sameTree(source, destination) {
17
17
  const left = stat(source);
18
18
  const right = stat(destination);
19
19
  if (!left || !right)
@@ -30,7 +30,7 @@ function sameTree(source, destination) {
30
30
  }
31
31
  return left.isFile() && right.isFile() && fs.readFileSync(source).equals(fs.readFileSync(destination));
32
32
  }
33
- function digest(file) {
33
+ export function digest(file) {
34
34
  const info = fs.lstatSync(file);
35
35
  const hash = crypto.createHash('sha256');
36
36
  if (info.isSymbolicLink())
@@ -125,12 +125,12 @@ export function inspectInstallation(configDir) {
125
125
  return { status: changed.length ? 'failed' : 'passed', packageVersion: value.packageVersion, agent: value.agent,
126
126
  summary: changed.length ? `changed or missing: ${changed.join(', ')}` : `${value.entries.length} installed definitions verified` };
127
127
  }
128
- function contains(parent, child) {
128
+ export function contains(parent, child) {
129
129
  const relative = path.relative(parent, child);
130
130
  return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
131
131
  }
132
132
  // Resolve existing ancestors too, including a config directory beneath a junction.
133
- function resolvedLocation(directory) {
133
+ export function resolvedLocation(directory) {
134
134
  if (fs.existsSync(directory))
135
135
  return fs.realpathSync(directory);
136
136
  const parent = path.dirname(directory);