android2harmony 0.1.6 → 0.1.8

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 (227) hide show
  1. package/agents/api-analyzer.md +113 -0
  2. package/agents/app-action.md +122 -0
  3. package/agents/code-reviewer.md +191 -295
  4. package/agents/coder.md +377 -0
  5. package/agents/self-tester.md +8 -8
  6. package/agents/spec-designer.md +56 -0
  7. package/package.json +1 -1
  8. package/skills/a2h-app-req-gen/SKILL.md +174 -0
  9. package/skills/a2h-app-req-gen/references/spec-handoff.md +99 -0
  10. package/skills/a2h-app-req-gen/references/subagent-render.md +115 -0
  11. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/build.gradle.kts +41 -0
  12. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.jar +0 -0
  13. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.properties +8 -0
  14. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle.properties +3 -0
  15. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew +251 -0
  16. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew.bat +94 -0
  17. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/settings.gradle.kts +17 -0
  18. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawAnalysisDetector.java +1911 -0
  19. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawIssueRegistry.java +37 -0
  20. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawJson.java +146 -0
  21. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawLocation.java +96 -0
  22. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawRecordWriter.java +227 -0
  23. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry +1 -0
  24. package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/dev/reqgen/analyzer/jvm/raw-record-v1.schema.json +93 -0
  25. package/skills/a2h-app-req-gen/scripts/reqgen/assets/dashboard.html +151 -0
  26. package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-lint.init.gradle +60 -0
  27. package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-model.init.gradle +204 -0
  28. package/skills/a2h-app-req-gen/scripts/reqgen/package.json +14 -0
  29. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/backend.js +323 -0
  30. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/cli.js +811 -0
  31. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/gap-audit-link.js +62 -0
  32. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/index.js +66 -0
  33. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/merge.js +605 -0
  34. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/name-trace.js +153 -0
  35. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/orchestrate.js +1324 -0
  36. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/output-parse.js +50 -0
  37. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/overview.js +10 -0
  38. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/pool.js +23 -0
  39. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/post-merge-fix.js +146 -0
  40. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt-loader.js +51 -0
  41. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt.js +967 -0
  42. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompts.md +301 -0
  43. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/serve.js +78 -0
  44. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/short-id.js +46 -0
  45. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/snapshot.js +267 -0
  46. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/types.js +1 -0
  47. package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/validate.js +122 -0
  48. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/callback-call-sites.js +62 -0
  49. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/build.js +1291 -0
  50. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/index.js +1 -0
  51. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/area-ownership.js +392 -0
  52. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/build.js +1995 -0
  53. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/callback-flow.js +277 -0
  54. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/component-variants.js +725 -0
  55. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/index.js +15 -0
  56. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/index.js +12 -0
  57. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/policy.js +469 -0
  58. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/traceability.js +468 -0
  59. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/build.js +1066 -0
  60. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/index.js +1 -0
  61. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android-interaction-registry.js +174 -0
  62. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android.js +1062 -0
  63. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/area-titles.js +678 -0
  64. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/builder.js +350 -0
  65. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-api-registry.js +395 -0
  66. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-call-sites.js +2008 -0
  67. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose.js +9059 -0
  68. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/dynamic-repeated-call-sites.js +529 -0
  69. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/index.js +1 -0
  70. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/interaction-effects.js +1149 -0
  71. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/jvm.js +6272 -0
  72. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/normalize.js +33 -0
  73. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/pending-intents.js +565 -0
  74. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/resource-id.js +42 -0
  75. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/route-state-variants.js +781 -0
  76. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/state.js +234 -0
  77. package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/types.js +1 -0
  78. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/extract.js +40 -0
  79. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/index.js +6 -0
  80. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/manifest.js +513 -0
  81. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/resources.js +398 -0
  82. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/types.js +1 -0
  83. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/views.js +447 -0
  84. package/skills/a2h-app-req-gen/scripts/reqgen/src/android/xml-parser.js +175 -0
  85. package/skills/a2h-app-req-gen/scripts/reqgen/src/cli.js +398 -0
  86. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/area-graph.js +1 -0
  87. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/artifact.js +1 -0
  88. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/common.js +16 -0
  89. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/diagnostic.js +10 -0
  90. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/gap-audit.js +14 -0
  91. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/index.js +11 -0
  92. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/name-traceability.js +26 -0
  93. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/raw-fact.js +40 -0
  94. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/req-model.js +1 -0
  95. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/run.js +19 -0
  96. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/semantic-completeness.js +11 -0
  97. package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/source-facts.js +1 -0
  98. package/skills/a2h-app-req-gen/scripts/reqgen/src/core/output-guard.js +41 -0
  99. package/skills/a2h-app-req-gen/scripts/reqgen/src/core/stable-id.js +92 -0
  100. package/skills/a2h-app-req-gen/scripts/reqgen/src/dashboard-cli.js +169 -0
  101. package/skills/a2h-app-req-gen/scripts/reqgen/src/diagnostics.js +104 -0
  102. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/dump.js +1028 -0
  103. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/index.js +1 -0
  104. package/skills/a2h-app-req-gen/scripts/reqgen/src/dump-cli.js +251 -0
  105. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/artifacts.js +49 -0
  106. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/index.js +18 -0
  107. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/quality-report.js +125 -0
  108. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/render.js +353 -0
  109. package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/types.js +2 -0
  110. package/skills/a2h-app-req-gen/scripts/reqgen/src/graph/paths.js +474 -0
  111. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/index.js +15 -0
  112. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/protocol.js +408 -0
  113. package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/runner.js +452 -0
  114. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/index.js +8 -0
  115. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/output.js +165 -0
  116. package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/run-stage1.js +891 -0
  117. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/discovery.js +299 -0
  118. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-model.js +358 -0
  119. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-runner.js +165 -0
  120. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/resource-profile.js +60 -0
  121. package/skills/a2h-app-req-gen/scripts/reqgen/src/project/types.js +1 -0
  122. package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff-cli.js +151 -0
  123. package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff.js +1004 -0
  124. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-cli.js +109 -0
  125. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-input.js +419 -0
  126. package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-name-trace-input.js +95 -0
  127. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/area-graph.js +444 -0
  128. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/index.js +7 -0
  129. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-model.js +580 -0
  130. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-text.js +195 -0
  131. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/semantic-area-graph.js +1431 -0
  132. package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/types.js +1 -0
  133. package/skills/a2h-app-req-gen/scripts/reqgen-audit.mjs +298 -0
  134. package/skills/a2h-app-req-gen/scripts/reqgen-selfcheck.mjs +555 -0
  135. package/skills/a2h-code-review/SKILL.md +379 -0
  136. package/skills/{hmos-integration-test → a2h-integration-test}/README.md +6 -6
  137. package/skills/{hmos-integration-test → a2h-integration-test}/SKILL.md +6 -6
  138. package/skills/{hmos-integration-test → a2h-integration-test}/scripts/report-tool.mjs +1 -1
  139. package/skills/a2h-spec-design/SKILL.md +330 -0
  140. package/skills/a2h-spec-design/scripts/inspect_spec_design_inputs.mjs +207 -0
  141. package/skills/a2h-spec-design/scripts/move_spec_technical_reference.mjs +198 -0
  142. package/skills/a2h-spec-design/scripts/validate_spec_design_output.mjs +256 -0
  143. package/skills/a2h-spec-generate/SKILL.md +696 -0
  144. package/skills/a2h-spec-generate/references/trace-template.md +115 -0
  145. package/skills/a2h-spec-generate/scripts/parse_requirements.mjs +537 -0
  146. package/skills/a2h-spec-generate/template/REQ.txt +22 -0
  147. package/skills/a2h-spec-generate/template/REQ.xlsx +0 -0
  148. package/skills/a2h-spec-implement/SKILL.md +548 -0
  149. package/skills/a2h-spec-implement/references/harmony-pitfalls.md +186 -0
  150. package/skills/a2h-spec-implement/references/l1-unit-test.md +185 -0
  151. package/skills/a2h-spec-implement/references/l3-e2e-uitest.md +167 -0
  152. package/skills/a2h-spec-implement/references/test-core.md +132 -0
  153. package/skills/a2h-spec-implement/rules/arkts/arkts-standards.md +184 -0
  154. package/skills/a2h-spec-implement/rules/arkts/conventions/coding-style.md +96 -0
  155. package/skills/a2h-spec-implement/rules/arkts/conventions/security.md +126 -0
  156. package/skills/a2h-spec-implement/rules/arkts/language/arkts-rules.md +650 -0
  157. package/skills/a2h-spec-implement/rules/arkts/language/arkui-structure-rules.md +173 -0
  158. package/skills/a2h-spec-implement/rules/arkts/language/ts-to-arkts-rewrites.md +58 -0
  159. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md +648 -0
  160. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md +2089 -0
  161. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md +1033 -0
  162. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md +1183 -0
  163. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md +576 -0
  164. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md +297 -0
  165. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md +395 -0
  166. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md +903 -0
  167. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md +106 -0
  168. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md +1178 -0
  169. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md +911 -0
  170. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +355 -0
  171. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v1//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md +11 -0
  172. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md +216 -0
  173. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md +442 -0
  174. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md +169 -0
  175. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md +763 -0
  176. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md +2088 -0
  177. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md +1258 -0
  178. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Once/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md +175 -0
  179. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Param/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md +850 -0
  180. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md +862 -0
  181. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md +110 -0
  182. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md +301 -0
  183. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md +1411 -0
  184. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/PersistenceV2/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md +1392 -0
  185. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/getTarget/346/216/245/345/217/243/357/274/232/350/216/267/345/217/226/347/212/266/346/200/201/347/256/241/347/220/206/346/241/206/346/236/266/344/273/243/347/220/206/345/211/215/347/232/204/345/216/237/345/247/213/345/257/271/350/261/241.md +288 -0
  186. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2/makeObserved/346/216/245/345/217/243/357/274/232/345/260/206/351/235/236/350/247/202/345/257/237/346/225/260/346/215/256/345/217/230/344/270/272/345/217/257/350/247/202/345/257/237/346/225/260/346/215/256.md +768 -0
  187. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/222/214V2/346/267/267/347/224/250/346/214/207/345/257/274/357/274/210API version 19/345/217/212/344/271/213/345/220/216/357/274/211.md" +829 -0
  188. package/skills/a2h-spec-implement/rules/arkts/official/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206/346/246/202/350/277/260.md +184 -0
  189. package/skills/a2h-spec-implement/rules/arkts/ui/component-cookbook.md +431 -0
  190. package/skills/a2h-spec-implement/rules/arkts/ui/state-management.md +152 -0
  191. package/skills/a2h-spec-implement/rules/arkts/ui/ui-quality.md +67 -0
  192. package/skills/a2h-spec-implement/rules/arkts/ui/ui-runtime-diagnosis.md +32 -0
  193. package/skills/a2h-spec-implement/rules/pipeline/build/package-set-collection.md +43 -0
  194. package/skills/a2h-spec-implement/scripts/_lib/common.mjs +97 -0
  195. package/skills/a2h-spec-implement/scripts/_lib/config.mjs +70 -0
  196. package/skills/a2h-spec-implement/scripts/_lib/lessons_cli.mjs +48 -0
  197. package/skills/a2h-spec-implement/scripts/build_lessons.mjs +24 -0
  198. package/skills/a2h-spec-implement/scripts/code_pattern_lessons.mjs +26 -0
  199. package/skills/a2h-spec-implement/scripts/device_ui.mjs +433 -0
  200. package/skills/a2h-spec-implement/scripts/device_ui_core.mjs +895 -0
  201. package/skills/a2h-spec-implement/scripts/ledger/aggregate_run.mjs +207 -0
  202. package/skills/a2h-spec-implement/scripts/manifest/emit_summary_line.mjs +37 -0
  203. package/skills/a2h-spec-implement/scripts/manifest/finalize_run.mjs +115 -0
  204. package/skills/a2h-spec-implement/scripts/manifest/render_final_summary.mjs +78 -0
  205. package/skills/a2h-spec-implement/scripts/manifest/root_mirror.mjs +137 -0
  206. package/skills/a2h-spec-implement/scripts/package.json +12 -0
  207. package/skills/a2h-spec-implement/scripts/state_propagation_check.mjs +676 -0
  208. package/skills/a2h-spec-implement/scripts/test/l2_run.mjs +244 -0
  209. package/skills/a2h-spec-implement/scripts/test/tp_coverage.mjs +231 -0
  210. package/skills/a2h-spec-implement/scripts/ui_memory.mjs +161 -0
  211. package/skills/a2h-spec-implement/scripts/utils/ensure_app.mjs +134 -0
  212. package/skills/a2h-ui-transfer/SKILL.md +20 -9
  213. package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +80 -0
  214. package/skills/hmos-convert-pipeline/SKILL.md +7 -5
  215. package/skills/hmos-incremental-ui-align/README.md +7 -7
  216. package/skills/hmos-incremental-ui-align/SKILL.md +1 -1
  217. package/skills/hmos-incremental-ui-align/page_align.md +1 -1
  218. package/skills/hmos-spec-generate/SKILL.md +2 -2
  219. package/skills/hmos-spec-generate/scripts/parse_requirements.mjs +537 -0
  220. package/skills/hmos-fix-build-errors/SKILL.md +0 -266
  221. package/skills/hmos-fix-build-errors/references/arkts-strict-patterns.md +0 -219
  222. package/skills/hmos-fix-build-errors/references/known-patterns.md +0 -157
  223. package/skills/hmos-fix-build-errors/references/rdb-entity-pattern.md +0 -131
  224. package/skills/hmos-spec-generate/scripts/parse_requirements.ts +0 -515
  225. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/resolve-metadata-tool.mjs +0 -0
  226. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/self-test-runner.mjs +0 -0
  227. /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/testcases-tool.mjs +0 -0
@@ -0,0 +1,1324 @@
1
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import {
5
+ buildAreaExpected,
6
+ buildAreaInputMd,
7
+ buildAreaTaskMdForSubagent,
8
+ buildFeatureExpected,
9
+ buildInputMd,
10
+ buildTaskMdForSubagent,
11
+ featureTitleParts,
12
+ hasUnresolvedPathSegmentNames,
13
+ } from "./prompt.js";
14
+ import { stripLeadingTitle } from "./merge.js";
15
+ import { runPool } from "./pool.js";
16
+ import { computeShortIds } from "./short-id.js";
17
+ import {
18
+ validateCitations,
19
+ validateRestatedFacts,
20
+ validateTitle,
21
+ validateTitleForMembers,
22
+ } from "./validate.js";
23
+ export function renderDirFor(requirementId) {
24
+ return requirementId.replace(/[^A-Za-z0-9_-]/g, "_");
25
+ }
26
+ const TOOL_CALL_PATTERN =
27
+ /message=evaluated permission=(grep|read|glob) pattern=(?:"((?:[^"\\]|\\.)*)"|(\S+))/g;
28
+ export function extractToolCalls(stderr) {
29
+ const calls = [];
30
+ for (const match of stderr.matchAll(TOOL_CALL_PATTERN)) {
31
+ const [, tool, quoted, bare] = match;
32
+ calls.push({
33
+ tool,
34
+ pattern: quoted !== undefined ? quoted.replace(/\\(.)/g, "$1") : bare,
35
+ });
36
+ }
37
+ return calls;
38
+ }
39
+ const RETRYABLE_REASONS = new Set(["execution-failed", "json-decode-error"]);
40
+ function buildRetryPrompt(errorReason, errorMessage) {
41
+ const description =
42
+ errorReason === "json-decode-error"
43
+ ? `你上一次的回复没有提供合法的 JSON 代码块(解析错误:${errorMessage})。`
44
+ : `你上一次的调用执行失败(错误:${errorMessage})。`;
45
+ return `${description}请重新按照最初的 "# Output" 一节要求,只输出一个合法的 \`\`\`json 代码块,代码块之外不要有任何文字。`;
46
+ }
47
+ async function defaultSleep(ms) {
48
+ await new Promise((resolve) => setTimeout(resolve, ms));
49
+ }
50
+ function planEvent(opts, perFeature, total) {
51
+ return {
52
+ phase: "plan",
53
+ total,
54
+ mode: opts.launchMode,
55
+ perFeature,
56
+ projectRoot: opts.projectRoot,
57
+ ...(opts.inputDirectory === undefined
58
+ ? {}
59
+ : { inputDirectory: opts.inputDirectory }),
60
+ ...(opts.outputDirectory === undefined
61
+ ? {}
62
+ : { outputDirectory: opts.outputDirectory }),
63
+ };
64
+ }
65
+ async function appendStatusEvent(renderDir, event, clock) {
66
+ await appendFile(
67
+ path.join(renderDir, "status.ndjson"),
68
+ `${JSON.stringify({ timestamp: clock(), ...event })}\n`,
69
+ "utf8",
70
+ );
71
+ }
72
+ async function validateOutput(
73
+ output,
74
+ item,
75
+ projectRoot,
76
+ attempt,
77
+ toolCalls,
78
+ nameTrace,
79
+ ) {
80
+ const parts = featureTitleParts(item, nameTrace);
81
+ const citationsResult = await validateCitations(
82
+ output.citations ?? [],
83
+ projectRoot,
84
+ );
85
+ const restatedResult = validateRestatedFacts(
86
+ output.restatedFacts ?? {},
87
+ item.feature,
88
+ );
89
+ const titleResult =
90
+ output.title === undefined
91
+ ? { valid: false, problems: ["title-missing"] }
92
+ : validateTitle(output.title, parts);
93
+ const warnings = [
94
+ ...citationsResult.invalid.map((inv) => ({ source: "citation", ...inv })),
95
+ ...restatedResult.mismatches.map((field) => ({
96
+ source: "restatedFacts",
97
+ field,
98
+ })),
99
+ ...titleResult.problems.map((reason) => ({
100
+ source: "title",
101
+ reason,
102
+ })),
103
+ ];
104
+ return {
105
+ ...output,
106
+ body:
107
+ output.body === undefined
108
+ ? undefined
109
+ : stripLeadingTitle(output.body, parts.title),
110
+ status: "ok",
111
+ titleValid: titleResult.valid,
112
+ warnings,
113
+ citationsValid: citationsResult.valid,
114
+ invalidCitations: citationsResult.invalid,
115
+ restatedFactsValid: restatedResult.valid,
116
+ restatedFactsMismatches: restatedResult.mismatches,
117
+ toolCalls,
118
+ attempt,
119
+ };
120
+ }
121
+ async function persistAttempt(dir, attempt, dispatch) {
122
+ await writeFile(
123
+ path.join(dir, `attempt-${attempt}.stdout`),
124
+ dispatch.stdout,
125
+ "utf8",
126
+ );
127
+ await writeFile(
128
+ path.join(dir, `attempt-${attempt}.stderr`),
129
+ dispatch.stderr,
130
+ "utf8",
131
+ );
132
+ if (dispatch.stats !== undefined) {
133
+ await writeFile(
134
+ path.join(dir, `attempt-${attempt}.stats.json`),
135
+ JSON.stringify(dispatch.stats, null, 2),
136
+ "utf8",
137
+ );
138
+ }
139
+ }
140
+ async function attemptOnce(item, ctx, prompt, sessionId, attempt) {
141
+ const dispatch = await ctx.opts.backend.run({
142
+ prompt,
143
+ cwd: ctx.opts.projectRoot,
144
+ agentName: ctx.opts.agentName ?? "reqgen-renderer",
145
+ ...(sessionId === undefined ? {} : { sessionId }),
146
+ });
147
+ const toolCalls = extractToolCalls(dispatch.stderr);
148
+ if (dispatch.status !== "ok") {
149
+ return {
150
+ dispatch,
151
+ result: {
152
+ status: "error",
153
+ errorReason: dispatch.errorReason,
154
+ error: dispatch.error,
155
+ citationsValid: false,
156
+ restatedFactsValid: false,
157
+ toolCalls,
158
+ attempt,
159
+ },
160
+ };
161
+ }
162
+ const result = await validateOutput(
163
+ dispatch.output,
164
+ item,
165
+ ctx.opts.projectRoot,
166
+ attempt,
167
+ toolCalls,
168
+ ctx.opts.nameTrace,
169
+ );
170
+ return { dispatch, result };
171
+ }
172
+ async function finalizeAttempt(
173
+ dir,
174
+ renderDir,
175
+ requirementId,
176
+ result,
177
+ clock,
178
+ stats,
179
+ ) {
180
+ // finalized mirrors the marker reqgen-selfcheck.mjs's `finalize` subcommand
181
+ // sets for subagent mode (ADR-0005) — only true on an actual success, so
182
+ // snapshot.ts's readAttempt treats the two modes' output.json identically.
183
+ const finalizedResult = { ...result, finalized: result.status === "ok" };
184
+ await writeFile(
185
+ path.join(dir, "output.json"),
186
+ JSON.stringify(finalizedResult, null, 2),
187
+ "utf8",
188
+ );
189
+ await appendStatusEvent(
190
+ renderDir,
191
+ {
192
+ phase: "finish",
193
+ requirementId,
194
+ status: result.status,
195
+ attempt: result.attempt,
196
+ ...(stats ? { stats } : {}),
197
+ },
198
+ clock,
199
+ );
200
+ return finalizedResult;
201
+ }
202
+ export async function renderFeatureToDisk(item, ctx) {
203
+ const maxRetries = ctx.opts.maxRetries ?? 5;
204
+ const backoffMs = ctx.opts.backoffMs ?? 5000;
205
+ const sleepFn = ctx.opts.sleepFn ?? defaultSleep;
206
+ const clock = ctx.opts.clock ?? Date.now;
207
+ const dir = path.join(ctx.opts.renderDir, renderDirFor(item.feature.id));
208
+ await mkdir(dir, { recursive: true });
209
+ const inputMd = buildInputMd(
210
+ item,
211
+ ctx.reqModel,
212
+ ctx.promptContext,
213
+ ctx.opts.gapAuditLinks,
214
+ ctx.opts.nameTrace,
215
+ );
216
+ await writeFile(path.join(dir, "input.md"), inputMd, "utf8");
217
+ let attempt = 1;
218
+ let sessionId;
219
+ let prompt = inputMd;
220
+ while (true) {
221
+ await appendStatusEvent(
222
+ ctx.opts.renderDir,
223
+ {
224
+ phase: "start",
225
+ requirementId: item.feature.id,
226
+ attempt,
227
+ },
228
+ clock,
229
+ );
230
+ const { dispatch, result } = await attemptOnce(
231
+ item,
232
+ ctx,
233
+ prompt,
234
+ sessionId,
235
+ attempt,
236
+ );
237
+ sessionId = dispatch.sessionId ?? sessionId;
238
+ await persistAttempt(dir, attempt, dispatch);
239
+ const canRetry =
240
+ dispatch.status !== "ok" &&
241
+ dispatch.errorReason !== undefined &&
242
+ RETRYABLE_REASONS.has(dispatch.errorReason) &&
243
+ attempt <= maxRetries;
244
+ if (!canRetry) {
245
+ return finalizeAttempt(
246
+ dir,
247
+ ctx.opts.renderDir,
248
+ item.feature.id,
249
+ result,
250
+ clock,
251
+ dispatch.stats,
252
+ );
253
+ }
254
+ const delayMs = backoffMs * 2 ** (attempt - 1);
255
+ await appendStatusEvent(
256
+ ctx.opts.renderDir,
257
+ {
258
+ phase: "retry-scheduled",
259
+ requirementId: item.feature.id,
260
+ attempt: attempt + 1,
261
+ reason: dispatch.errorReason,
262
+ delayMs,
263
+ retryAt: clock() + delayMs,
264
+ },
265
+ clock,
266
+ );
267
+ await sleepFn(delayMs);
268
+ prompt = buildRetryPrompt(dispatch.errorReason, dispatch.error);
269
+ attempt += 1;
270
+ }
271
+ }
272
+ export { runPool } from "./pool.js";
273
+ export function expandRenderItems(reqModel) {
274
+ return reqModel.areas.flatMap((area) =>
275
+ area.features.map((feature) => ({ area, feature })),
276
+ );
277
+ }
278
+ export async function renderAllFeatures(items, reqModel, opts) {
279
+ const clock = opts.clock ?? Date.now;
280
+ await mkdir(opts.renderDir, { recursive: true });
281
+ await writeFile(path.join(opts.renderDir, "status.ndjson"), "", "utf8");
282
+ await appendStatusEvent(
283
+ opts.renderDir,
284
+ planEvent(opts, true, items.length),
285
+ clock,
286
+ );
287
+ const ctx = {
288
+ reqModel,
289
+ promptContext: {
290
+ projectRoot: opts.projectRoot,
291
+ module: reqModel.analysisContext.applicationModule,
292
+ variant: reqModel.analysisContext.buildVariant,
293
+ analysisMode: opts.analysisMode,
294
+ },
295
+ opts,
296
+ };
297
+ const results = new Map();
298
+ await runPool(items, opts.concurrency ?? 4, async (item) => {
299
+ let result;
300
+ try {
301
+ result = await renderFeatureToDisk(item, ctx);
302
+ } catch (error) {
303
+ result = {
304
+ status: "error",
305
+ error: error instanceof Error ? error.message : String(error),
306
+ citationsValid: false,
307
+ restatedFactsValid: false,
308
+ attempt: 0,
309
+ };
310
+ await appendStatusEvent(
311
+ opts.renderDir,
312
+ {
313
+ phase: "finish",
314
+ requirementId: item.feature.id,
315
+ status: result.status,
316
+ },
317
+ clock,
318
+ );
319
+ }
320
+ results.set(item.feature.id, result);
321
+ });
322
+ return results;
323
+ }
324
+ // =============================================================================
325
+ // Per-Area rendering (one session per Area)
326
+ // =============================================================================
327
+ export function areaDirFor(areaId) {
328
+ return areaId.replace(/[^A-Za-z0-9_-]/g, "_");
329
+ }
330
+ function resolveAreaKind(area) {
331
+ if (area.canonicalPath.status === "unreachable") return "unreachable";
332
+ if (hasUnresolvedPathSegmentNames(area)) return "path-needs-names";
333
+ return "normal";
334
+ }
335
+ export function expandAreaRenderItems(reqModel) {
336
+ return reqModel.areas.map((area) => ({ area }));
337
+ }
338
+ /** Every requirementId a single AreaRenderOutput.features entry covers: just
339
+ * its own `requirementId` normally, or every id listed in
340
+ * `mergedRequirementIds` (including its own) when the Agent merged several
341
+ * Controls into one req item — see FeatureRenderResult.mergedRequirementIds. */
342
+ function coveredRequirementIds(entry) {
343
+ return entry.mergedRequirementIds !== undefined &&
344
+ entry.mergedRequirementIds.length > 0
345
+ ? entry.mergedRequirementIds
346
+ : [entry.requirementId ?? ""];
347
+ }
348
+ function isAgentAddedFeature(entry) {
349
+ return entry.kind === "agent-added";
350
+ }
351
+ async function validateAgentAddedFeature(featureOutput, projectRoot) {
352
+ const citations = featureOutput.citations ?? [];
353
+ const citationsResult = await validateCitations(citations, projectRoot);
354
+ const title = featureOutput.title?.trim();
355
+ const body = featureOutput.body?.trim();
356
+ const error =
357
+ title === undefined || title.length === 0
358
+ ? "agent-added-feature-title-missing"
359
+ : body === undefined || body.length === 0
360
+ ? "agent-added-feature-body-missing"
361
+ : citations.length === 0
362
+ ? "agent-added-feature-citations-missing"
363
+ : citationsResult.valid
364
+ ? undefined
365
+ : "agent-added-feature-citations-invalid";
366
+ return {
367
+ status: error === undefined ? "ok" : "error",
368
+ ...(error === undefined ? {} : { error }),
369
+ ...(title === undefined || title.length === 0
370
+ ? {}
371
+ : { title: featureOutput.title }),
372
+ ...(body === undefined || body.length === 0
373
+ ? {}
374
+ : { body: featureOutput.body }),
375
+ ...(featureOutput.citations === undefined
376
+ ? {}
377
+ : { citations: featureOutput.citations }),
378
+ ...(featureOutput.confidence === undefined
379
+ ? {}
380
+ : { confidence: featureOutput.confidence }),
381
+ citationsValid: citationsResult.valid,
382
+ ...(citationsResult.invalid.length === 0
383
+ ? {}
384
+ : { invalidCitations: citationsResult.invalid }),
385
+ };
386
+ }
387
+ /** `members` is every Feature this one output entry covers, in Area.features
388
+ * order — length 1 for the ordinary one-Feature case, length > 1 for a
389
+ * merged req item. restatedFacts is checked against `members[0]` (the
390
+ * earliest member in Area order, deterministic regardless of how the Agent
391
+ * ordered mergedRequirementIds); title is checked against every member's own
392
+ * name-inclusion requirement (validateTitleForMembers) so a merged title
393
+ * must still name each Control it covers. */
394
+ async function validateFeatureOutput(
395
+ featureOutput,
396
+ members,
397
+ projectRoot,
398
+ nameTrace,
399
+ ) {
400
+ const citationsResult = await validateCitations(
401
+ featureOutput.citations ?? [],
402
+ projectRoot,
403
+ );
404
+ const restatedResult = validateRestatedFacts(
405
+ featureOutput.restatedFacts ?? {},
406
+ members[0].feature,
407
+ );
408
+ const partsList = members.map((member) =>
409
+ featureTitleParts(member, nameTrace),
410
+ );
411
+ const titleResult =
412
+ featureOutput.title === undefined
413
+ ? { valid: false, problems: ["title-missing"] }
414
+ : validateTitleForMembers(featureOutput.title, partsList);
415
+ const warnings = [
416
+ ...citationsResult.invalid.map((inv) => ({ source: "citation", ...inv })),
417
+ ...restatedResult.mismatches.map((field) => ({
418
+ source: "restatedFacts",
419
+ field,
420
+ })),
421
+ ...titleResult.problems.map((reason) => ({
422
+ source: "title",
423
+ reason,
424
+ })),
425
+ ];
426
+ // A merged entry's echoed leading-title-line (if any) is the Agent's own
427
+ // merged title, not any single member's individually computed template —
428
+ // there is no one template that represents the whole group.
429
+ const stripTitle =
430
+ members.length > 1
431
+ ? (featureOutput.title ?? partsList[0].title)
432
+ : partsList[0].title;
433
+ return {
434
+ status: "ok",
435
+ title: featureOutput.title,
436
+ titleValid: titleResult.valid,
437
+ body:
438
+ featureOutput.body === undefined
439
+ ? undefined
440
+ : stripLeadingTitle(featureOutput.body, stripTitle),
441
+ restatedFacts: featureOutput.restatedFacts,
442
+ citations: featureOutput.citations,
443
+ confidence: featureOutput.confidence,
444
+ citationsValid: citationsResult.valid,
445
+ invalidCitations: citationsResult.invalid,
446
+ restatedFactsValid: restatedResult.valid,
447
+ restatedFactsMismatches: restatedResult.mismatches,
448
+ warnings,
449
+ ...(members.length > 1
450
+ ? { mergedFeatureIds: members.map((m) => m.feature.id) }
451
+ : {}),
452
+ };
453
+ }
454
+ async function validateAreaOutput(output, item, projectRoot, nameTrace) {
455
+ const results = new Map();
456
+ const shortIds = computeShortIds(item.area.features);
457
+ const outputFeatures = output.features ?? [];
458
+ const hasFeatureError =
459
+ item.area.features.length === 0 && outputFeatures.length === 0
460
+ ? output.hasFeature === false
461
+ ? undefined
462
+ : "has-feature-marker-missing-in-agent-output"
463
+ : output.hasFeature === false
464
+ ? "has-feature-marker-conflicts-with-features"
465
+ : undefined;
466
+ const addedFeatures = await Promise.all(
467
+ outputFeatures
468
+ .filter(isAgentAddedFeature)
469
+ .map((featureOutput) =>
470
+ validateAgentAddedFeature(featureOutput, projectRoot),
471
+ ),
472
+ );
473
+ const byRequirementId = new Map();
474
+ for (const entry of outputFeatures) {
475
+ if (isAgentAddedFeature(entry)) continue;
476
+ for (const shortId of coveredRequirementIds(entry)) {
477
+ const group = byRequirementId.get(shortId);
478
+ if (group === undefined) {
479
+ byRequirementId.set(shortId, [entry]);
480
+ } else {
481
+ group.push(entry);
482
+ }
483
+ }
484
+ }
485
+ // A merge group's single FeatureOutput is validated once and its result
486
+ // shared by every member id it covers, instead of re-running citation/
487
+ // title checks once per member.
488
+ const evaluated = new Map();
489
+ for (const [index, feature] of item.area.features.entries()) {
490
+ const shortId = shortIds.get(feature.id);
491
+ const group = byRequirementId.get(shortId);
492
+ if (group === undefined) {
493
+ results.set(feature.id, {
494
+ status: "error",
495
+ error: "feature-missing-in-agent-output",
496
+ citationsValid: false,
497
+ restatedFactsValid: false,
498
+ });
499
+ continue;
500
+ }
501
+ if (group.length > 1) {
502
+ results.set(feature.id, {
503
+ status: "error",
504
+ error: "feature-id-collision-in-agent-output",
505
+ citationsValid: false,
506
+ restatedFactsValid: false,
507
+ });
508
+ continue;
509
+ }
510
+ const featureOutput = group[0];
511
+ const coveredIds = coveredRequirementIds(featureOutput);
512
+ const merged = coveredIds.length > 1;
513
+ // Redundant cross-check: requirementId is a real id, but a real id can
514
+ // still be attached to the wrong Feature's content (correct id, wrong
515
+ // pairing) without any duplicate ever occurring — controlIndex is an
516
+ // independent signal that must agree with the enumeration position this
517
+ // requirementId was given at. A disagreement here is exactly the
518
+ // PermissionAuthorizationDialog/UsbStorageFolderDialog mixup, caught
519
+ // instead of silently merged. Omitted controlIndex (older/custom
520
+ // backends) is not treated as a mismatch. Skipped for a merged entry —
521
+ // its controlIndex names just one representative member, not every
522
+ // covered position, so it is not a meaningful cross-check there.
523
+ if (
524
+ !merged &&
525
+ featureOutput.controlIndex !== undefined &&
526
+ featureOutput.controlIndex !== index + 1
527
+ ) {
528
+ results.set(feature.id, {
529
+ status: "error",
530
+ error: "feature-index-mismatch-in-agent-output",
531
+ citationsValid: false,
532
+ restatedFactsValid: false,
533
+ });
534
+ continue;
535
+ }
536
+ let evaluation = evaluated.get(featureOutput);
537
+ if (evaluation === undefined) {
538
+ const coveredIdSet = new Set(coveredIds);
539
+ const members = item.area.features
540
+ .filter((f) => coveredIdSet.has(shortIds.get(f.id)))
541
+ .map((f) => ({ area: item.area, feature: f }));
542
+ evaluation = validateFeatureOutput(
543
+ featureOutput,
544
+ members.length > 0 ? members : [{ area: item.area, feature }],
545
+ projectRoot,
546
+ nameTrace,
547
+ );
548
+ evaluated.set(featureOutput, evaluation);
549
+ }
550
+ results.set(feature.id, await evaluation);
551
+ }
552
+ return {
553
+ features: results,
554
+ addedFeatures,
555
+ ...(output.hasFeature === false && hasFeatureError === undefined
556
+ ? { hasFeature: false }
557
+ : {}),
558
+ ...(hasFeatureError === undefined ? {} : { error: hasFeatureError }),
559
+ };
560
+ }
561
+ async function attemptAreaOnce(item, ctx, prompt, sessionId, attempt) {
562
+ const dispatch = await ctx.opts.backend.run({
563
+ prompt,
564
+ cwd: ctx.opts.projectRoot,
565
+ agentName: ctx.opts.agentName ?? "reqgen-renderer",
566
+ ...(sessionId === undefined ? {} : { sessionId }),
567
+ });
568
+ const toolCalls = extractToolCalls(dispatch.stderr);
569
+ if (dispatch.status !== "ok") {
570
+ return {
571
+ dispatch,
572
+ result: {
573
+ status: "error",
574
+ errorReason: dispatch.errorReason,
575
+ error: dispatch.error,
576
+ areaId: item.area.id,
577
+ features: new Map(),
578
+ toolCalls,
579
+ attempt,
580
+ },
581
+ };
582
+ }
583
+ const output = dispatch.output;
584
+ const validated = await validateAreaOutput(
585
+ output,
586
+ item,
587
+ ctx.opts.projectRoot,
588
+ ctx.opts.nameTrace,
589
+ );
590
+ if (validated.error !== undefined) {
591
+ return {
592
+ dispatch,
593
+ result: {
594
+ status: "error",
595
+ error: validated.error,
596
+ areaId: item.area.id,
597
+ features: new Map(),
598
+ toolCalls,
599
+ attempt,
600
+ },
601
+ };
602
+ }
603
+ return {
604
+ dispatch,
605
+ result: {
606
+ status: "ok",
607
+ areaId: item.area.id,
608
+ ...(output.areaDescription === undefined
609
+ ? {}
610
+ : { areaDescription: output.areaDescription }),
611
+ ...(output.accessPath === undefined
612
+ ? {}
613
+ : { accessPath: output.accessPath }),
614
+ ...(output.reachable === undefined
615
+ ? {}
616
+ : { reachable: output.reachable }),
617
+ features: validated.features,
618
+ ...(validated.addedFeatures.length === 0
619
+ ? {}
620
+ : { addedFeatures: validated.addedFeatures }),
621
+ ...(validated.hasFeature === false ? { hasFeature: false } : {}),
622
+ toolCalls,
623
+ attempt,
624
+ },
625
+ };
626
+ }
627
+ async function finalizeAreaAttempt(dir, renderDir, result, clock, stats) {
628
+ // Convert Map to plain object for JSON serialization
629
+ const featuresObj = {};
630
+ for (const [id, featureResult] of result.features) {
631
+ featuresObj[id] = featureResult;
632
+ }
633
+ // finalized mirrors the marker reqgen-selfcheck.mjs's `finalize` subcommand
634
+ // sets for subagent mode (ADR-0005) — only true on an actual success, so
635
+ // snapshot.ts's readAreaAttempt treats the two modes' output.json
636
+ // identically.
637
+ const serializable = {
638
+ ...result,
639
+ finalized: result.status === "ok",
640
+ features: featuresObj,
641
+ };
642
+ await writeFile(
643
+ path.join(dir, "output.json"),
644
+ JSON.stringify(serializable, null, 2),
645
+ "utf8",
646
+ );
647
+ await appendStatusEvent(
648
+ renderDir,
649
+ {
650
+ phase: "finish",
651
+ areaId: result.areaId,
652
+ status: result.status,
653
+ attempt: result.attempt,
654
+ featureCount: result.features.size,
655
+ ...(stats ? { stats } : {}),
656
+ },
657
+ clock,
658
+ );
659
+ return { ...result, finalized: result.status === "ok" };
660
+ }
661
+ export async function renderAreaToDisk(item, ctx) {
662
+ const maxRetries = ctx.opts.maxRetries ?? 5;
663
+ const backoffMs = ctx.opts.backoffMs ?? 5000;
664
+ const sleepFn = ctx.opts.sleepFn ?? defaultSleep;
665
+ const clock = ctx.opts.clock ?? Date.now;
666
+ const dir = path.join(ctx.opts.renderDir, areaDirFor(item.area.id));
667
+ await mkdir(dir, { recursive: true });
668
+ const inputMd = buildAreaInputMd(
669
+ item,
670
+ ctx.reqModel,
671
+ ctx.promptContext,
672
+ ctx.opts.gapAuditLinks,
673
+ ctx.opts.nameTrace,
674
+ );
675
+ await writeFile(path.join(dir, "input.md"), inputMd, "utf8");
676
+ const kind = resolveAreaKind(item.area);
677
+ let attempt = 1;
678
+ let sessionId;
679
+ let prompt = inputMd;
680
+ while (true) {
681
+ await appendStatusEvent(
682
+ ctx.opts.renderDir,
683
+ {
684
+ phase: "start",
685
+ areaId: item.area.id,
686
+ attempt,
687
+ featureCount: item.area.features.length,
688
+ kind,
689
+ },
690
+ clock,
691
+ );
692
+ const { dispatch, result } = await attemptAreaOnce(
693
+ item,
694
+ ctx,
695
+ prompt,
696
+ sessionId,
697
+ attempt,
698
+ );
699
+ sessionId = dispatch.sessionId ?? sessionId;
700
+ await persistAttempt(dir, attempt, dispatch);
701
+ const canRetry =
702
+ dispatch.status !== "ok" &&
703
+ dispatch.errorReason !== undefined &&
704
+ RETRYABLE_REASONS.has(dispatch.errorReason) &&
705
+ attempt <= maxRetries;
706
+ if (!canRetry) {
707
+ return finalizeAreaAttempt(
708
+ dir,
709
+ ctx.opts.renderDir,
710
+ result,
711
+ clock,
712
+ dispatch.stats,
713
+ );
714
+ }
715
+ const delayMs = backoffMs * 2 ** (attempt - 1);
716
+ await appendStatusEvent(
717
+ ctx.opts.renderDir,
718
+ {
719
+ phase: "retry-scheduled",
720
+ areaId: item.area.id,
721
+ attempt: attempt + 1,
722
+ reason: dispatch.errorReason,
723
+ delayMs,
724
+ retryAt: clock() + delayMs,
725
+ },
726
+ clock,
727
+ );
728
+ await sleepFn(delayMs);
729
+ prompt = buildRetryPrompt(dispatch.errorReason, dispatch.error);
730
+ attempt += 1;
731
+ }
732
+ }
733
+ export async function renderAllAreas(items, reqModel, opts) {
734
+ const clock = opts.clock ?? Date.now;
735
+ await mkdir(opts.renderDir, { recursive: true });
736
+ await writeFile(path.join(opts.renderDir, "status.ndjson"), "", "utf8");
737
+ await appendStatusEvent(
738
+ opts.renderDir,
739
+ planEvent(opts, false, items.length),
740
+ clock,
741
+ );
742
+ const ctx = {
743
+ reqModel,
744
+ promptContext: {
745
+ projectRoot: opts.projectRoot,
746
+ module: reqModel.analysisContext.applicationModule,
747
+ variant: reqModel.analysisContext.buildVariant,
748
+ analysisMode: opts.analysisMode,
749
+ },
750
+ opts,
751
+ };
752
+ const results = new Map();
753
+ await runPool(items, opts.concurrency ?? 4, async (item) => {
754
+ let result;
755
+ try {
756
+ result = await renderAreaToDisk(item, ctx);
757
+ } catch (error) {
758
+ result = {
759
+ status: "error",
760
+ error: error instanceof Error ? error.message : String(error),
761
+ areaId: item.area.id,
762
+ features: new Map(),
763
+ attempt: 0,
764
+ };
765
+ await appendStatusEvent(
766
+ opts.renderDir,
767
+ {
768
+ phase: "finish",
769
+ areaId: item.area.id,
770
+ status: result.status,
771
+ },
772
+ clock,
773
+ );
774
+ }
775
+ results.set(item.area.id, result);
776
+ });
777
+ return results;
778
+ }
779
+ // =============================================================================
780
+ // Unified protocol dispatch (ADR-0005, 2026-07-25 revision): writes task.md +
781
+ // expected.json via the same prepare builders as subagent mode, spawns backend
782
+ // with the taskPath, and checks output.json on disk for finalized:true instead
783
+ // of parsing stdout. Only the claude backend uses this path; opencode and
784
+ // --ai-command keep the legacy inline-prompt + validate.ts path.
785
+ // =============================================================================
786
+ async function readFinalizedFeatureOutput(outputPath) {
787
+ try {
788
+ const parsed = JSON.parse(await readFile(outputPath, "utf8"));
789
+ if (
790
+ parsed.finalized === true &&
791
+ parsed.status === "ok" &&
792
+ typeof parsed.body === "string"
793
+ ) {
794
+ return parsed;
795
+ }
796
+ } catch {
797
+ /* file missing or unreadable — not finalized */
798
+ }
799
+ return undefined;
800
+ }
801
+ async function readFinalizedAreaOutput(outputPath) {
802
+ try {
803
+ const parsed = JSON.parse(await readFile(outputPath, "utf8"));
804
+ if (parsed.status !== "ok" || parsed.finalized !== true) return undefined;
805
+ const features = new Map();
806
+ for (const [id, fr] of Object.entries(parsed.features ?? {})) {
807
+ if (fr.status === "ok" && typeof fr.body === "string")
808
+ features.set(id, fr);
809
+ }
810
+ return {
811
+ status: "ok",
812
+ areaId: parsed.areaId ?? "",
813
+ ...(parsed.areaDescription === undefined
814
+ ? {}
815
+ : { areaDescription: parsed.areaDescription }),
816
+ ...(parsed.accessPath === undefined
817
+ ? {}
818
+ : { accessPath: parsed.accessPath }),
819
+ ...(parsed.reachable === undefined
820
+ ? {}
821
+ : { reachable: parsed.reachable }),
822
+ features,
823
+ ...(parsed.addedFeatures === undefined
824
+ ? {}
825
+ : { addedFeatures: parsed.addedFeatures }),
826
+ ...(parsed.hasFeature === false ? { hasFeature: false } : {}),
827
+ attempt: parsed.attempt ?? 1,
828
+ };
829
+ } catch {
830
+ /* file missing or unreadable */
831
+ }
832
+ return undefined;
833
+ }
834
+ export async function renderFeatureUnified(item, ctx) {
835
+ const maxRetries = ctx.opts.maxRetries ?? 5;
836
+ const backoffMs = ctx.opts.backoffMs ?? 5000;
837
+ const sleepFn = ctx.opts.sleepFn ?? defaultSleep;
838
+ const clock = ctx.opts.clock ?? Date.now;
839
+ const dir = path.join(ctx.opts.renderDir, renderDirFor(item.feature.id));
840
+ await mkdir(dir, { recursive: true });
841
+ const checkScriptPath = defaultCheckScriptPath();
842
+ const outputPath = path.join(dir, "output.json");
843
+ const expectedPath = path.join(dir, "expected.json");
844
+ const taskPath = path.join(dir, "task.md");
845
+ const paths = { expectedPath, outputPath, checkScriptPath };
846
+ if (ctx.opts.resume) {
847
+ const existing = await readFinalizedFeatureOutput(outputPath);
848
+ if (existing !== undefined) {
849
+ await appendStatusEvent(
850
+ ctx.opts.renderDir,
851
+ {
852
+ phase: "finish",
853
+ requirementId: item.feature.id,
854
+ status: "ok",
855
+ attempt: existing.attempt,
856
+ resumed: true,
857
+ },
858
+ clock,
859
+ );
860
+ return existing;
861
+ }
862
+ }
863
+ const taskMd = buildTaskMdForSubagent(
864
+ item,
865
+ ctx.reqModel,
866
+ ctx.promptContext,
867
+ ctx.opts.gapAuditLinks,
868
+ paths,
869
+ ctx.opts.nameTrace,
870
+ );
871
+ await writeFile(taskPath, taskMd, "utf8");
872
+ await writeFile(
873
+ expectedPath,
874
+ JSON.stringify(buildFeatureExpected(item, ctx.opts.nameTrace), null, 2),
875
+ "utf8",
876
+ );
877
+ for (let attempt = 1; ; attempt += 1) {
878
+ await appendStatusEvent(
879
+ ctx.opts.renderDir,
880
+ {
881
+ phase: "start",
882
+ requirementId: item.feature.id,
883
+ attempt,
884
+ },
885
+ clock,
886
+ );
887
+ const dispatch = await ctx.opts.backend.run({
888
+ prompt: taskPath,
889
+ cwd: ctx.opts.projectRoot,
890
+ agentName: ctx.opts.agentName ?? "reqgen-renderer",
891
+ });
892
+ await persistAttempt(dir, attempt, dispatch);
893
+ const result = await readFinalizedFeatureOutput(outputPath);
894
+ if (result !== undefined) {
895
+ await appendStatusEvent(
896
+ ctx.opts.renderDir,
897
+ {
898
+ phase: "finish",
899
+ requirementId: item.feature.id,
900
+ status: "ok",
901
+ attempt,
902
+ ...(dispatch.stats ? { stats: dispatch.stats } : {}),
903
+ },
904
+ clock,
905
+ );
906
+ return result;
907
+ }
908
+ if (attempt > maxRetries) {
909
+ const errorResult = {
910
+ status: "error",
911
+ error:
912
+ dispatch.status !== "ok"
913
+ ? (dispatch.error ?? "backend process failed")
914
+ : "subprocess exited but output.json was not finalized",
915
+ citationsValid: false,
916
+ restatedFactsValid: false,
917
+ attempt,
918
+ };
919
+ await writeFile(outputPath, JSON.stringify(errorResult, null, 2), "utf8");
920
+ await appendStatusEvent(
921
+ ctx.opts.renderDir,
922
+ {
923
+ phase: "finish",
924
+ requirementId: item.feature.id,
925
+ status: "error",
926
+ attempt,
927
+ },
928
+ clock,
929
+ );
930
+ return errorResult;
931
+ }
932
+ const delayMs = backoffMs * 2 ** (attempt - 1);
933
+ await appendStatusEvent(
934
+ ctx.opts.renderDir,
935
+ {
936
+ phase: "retry-scheduled",
937
+ requirementId: item.feature.id,
938
+ attempt: attempt + 1,
939
+ reason:
940
+ dispatch.status !== "ok"
941
+ ? (dispatch.errorReason ?? "execution-failed")
942
+ : "output-not-finalized",
943
+ delayMs,
944
+ retryAt: clock() + delayMs,
945
+ },
946
+ clock,
947
+ );
948
+ await sleepFn(delayMs);
949
+ }
950
+ }
951
+ export async function renderAreaUnified(item, ctx) {
952
+ const maxRetries = ctx.opts.maxRetries ?? 5;
953
+ const backoffMs = ctx.opts.backoffMs ?? 5000;
954
+ const sleepFn = ctx.opts.sleepFn ?? defaultSleep;
955
+ const clock = ctx.opts.clock ?? Date.now;
956
+ const dir = path.join(ctx.opts.renderDir, areaDirFor(item.area.id));
957
+ await mkdir(dir, { recursive: true });
958
+ const checkScriptPath = defaultCheckScriptPath();
959
+ const outputPath = path.join(dir, "output.json");
960
+ const expectedPath = path.join(dir, "expected.json");
961
+ const taskPath = path.join(dir, "task.md");
962
+ const paths = { expectedPath, outputPath, checkScriptPath };
963
+ if (ctx.opts.resume) {
964
+ const existing = await readFinalizedAreaOutput(outputPath);
965
+ if (existing !== undefined) {
966
+ await appendStatusEvent(
967
+ ctx.opts.renderDir,
968
+ {
969
+ phase: "finish",
970
+ areaId: item.area.id,
971
+ status: "ok",
972
+ attempt: existing.attempt,
973
+ featureCount: existing.features.size,
974
+ resumed: true,
975
+ },
976
+ clock,
977
+ );
978
+ return existing;
979
+ }
980
+ }
981
+ const taskMd = buildAreaTaskMdForSubagent(
982
+ item,
983
+ ctx.reqModel,
984
+ ctx.promptContext,
985
+ ctx.opts.gapAuditLinks,
986
+ paths,
987
+ ctx.opts.nameTrace,
988
+ );
989
+ await writeFile(taskPath, taskMd, "utf8");
990
+ await writeFile(
991
+ expectedPath,
992
+ JSON.stringify(buildAreaExpected(item, ctx.opts.nameTrace), null, 2),
993
+ "utf8",
994
+ );
995
+ const kind = resolveAreaKind(item.area);
996
+ for (let attempt = 1; ; attempt += 1) {
997
+ await appendStatusEvent(
998
+ ctx.opts.renderDir,
999
+ {
1000
+ phase: "start",
1001
+ areaId: item.area.id,
1002
+ attempt,
1003
+ featureCount: item.area.features.length,
1004
+ kind,
1005
+ },
1006
+ clock,
1007
+ );
1008
+ const dispatch = await ctx.opts.backend.run({
1009
+ prompt: taskPath,
1010
+ cwd: ctx.opts.projectRoot,
1011
+ agentName: ctx.opts.agentName ?? "reqgen-renderer",
1012
+ });
1013
+ await persistAttempt(dir, attempt, dispatch);
1014
+ const result = await readFinalizedAreaOutput(outputPath);
1015
+ if (result !== undefined) {
1016
+ await appendStatusEvent(
1017
+ ctx.opts.renderDir,
1018
+ {
1019
+ phase: "finish",
1020
+ areaId: item.area.id,
1021
+ status: "ok",
1022
+ attempt,
1023
+ featureCount: result.features.size,
1024
+ ...(dispatch.stats ? { stats: dispatch.stats } : {}),
1025
+ },
1026
+ clock,
1027
+ );
1028
+ return result;
1029
+ }
1030
+ if (attempt > maxRetries) {
1031
+ const errorResult = {
1032
+ status: "error",
1033
+ error:
1034
+ dispatch.status !== "ok"
1035
+ ? (dispatch.error ?? "backend process failed")
1036
+ : "subprocess exited but output.json was not finalized",
1037
+ areaId: item.area.id,
1038
+ features: new Map(),
1039
+ attempt,
1040
+ };
1041
+ const featuresObj = {};
1042
+ await writeFile(
1043
+ outputPath,
1044
+ JSON.stringify(
1045
+ { ...errorResult, finalized: false, features: featuresObj },
1046
+ null,
1047
+ 2,
1048
+ ),
1049
+ "utf8",
1050
+ );
1051
+ await appendStatusEvent(
1052
+ ctx.opts.renderDir,
1053
+ {
1054
+ phase: "finish",
1055
+ areaId: item.area.id,
1056
+ status: "error",
1057
+ attempt,
1058
+ },
1059
+ clock,
1060
+ );
1061
+ return errorResult;
1062
+ }
1063
+ const delayMs = backoffMs * 2 ** (attempt - 1);
1064
+ await appendStatusEvent(
1065
+ ctx.opts.renderDir,
1066
+ {
1067
+ phase: "retry-scheduled",
1068
+ areaId: item.area.id,
1069
+ attempt: attempt + 1,
1070
+ reason:
1071
+ dispatch.status !== "ok"
1072
+ ? (dispatch.errorReason ?? "execution-failed")
1073
+ : "output-not-finalized",
1074
+ delayMs,
1075
+ retryAt: clock() + delayMs,
1076
+ },
1077
+ clock,
1078
+ );
1079
+ await sleepFn(delayMs);
1080
+ }
1081
+ }
1082
+ export async function renderAllFeaturesUnified(items, reqModel, opts) {
1083
+ const clock = opts.clock ?? Date.now;
1084
+ await mkdir(opts.renderDir, { recursive: true });
1085
+ if (!opts.resume)
1086
+ await writeFile(path.join(opts.renderDir, "status.ndjson"), "", "utf8");
1087
+ await appendStatusEvent(
1088
+ opts.renderDir,
1089
+ planEvent(opts, true, items.length),
1090
+ clock,
1091
+ );
1092
+ const ctx = {
1093
+ reqModel,
1094
+ promptContext: {
1095
+ projectRoot: opts.projectRoot,
1096
+ module: reqModel.analysisContext.applicationModule,
1097
+ variant: reqModel.analysisContext.buildVariant,
1098
+ analysisMode: opts.analysisMode,
1099
+ },
1100
+ opts,
1101
+ };
1102
+ const results = new Map();
1103
+ await runPool(items, opts.concurrency ?? 4, async (item) => {
1104
+ let result;
1105
+ try {
1106
+ result = await renderFeatureUnified(item, ctx);
1107
+ } catch (error) {
1108
+ result = {
1109
+ status: "error",
1110
+ error: error instanceof Error ? error.message : String(error),
1111
+ citationsValid: false,
1112
+ restatedFactsValid: false,
1113
+ attempt: 0,
1114
+ };
1115
+ await appendStatusEvent(
1116
+ opts.renderDir,
1117
+ {
1118
+ phase: "finish",
1119
+ requirementId: item.feature.id,
1120
+ status: result.status,
1121
+ },
1122
+ clock,
1123
+ );
1124
+ }
1125
+ results.set(item.feature.id, result);
1126
+ });
1127
+ return results;
1128
+ }
1129
+ export async function renderAllAreasUnified(items, reqModel, opts) {
1130
+ const clock = opts.clock ?? Date.now;
1131
+ await mkdir(opts.renderDir, { recursive: true });
1132
+ if (!opts.resume)
1133
+ await writeFile(path.join(opts.renderDir, "status.ndjson"), "", "utf8");
1134
+ await appendStatusEvent(
1135
+ opts.renderDir,
1136
+ planEvent(opts, false, items.length),
1137
+ clock,
1138
+ );
1139
+ const ctx = {
1140
+ reqModel,
1141
+ promptContext: {
1142
+ projectRoot: opts.projectRoot,
1143
+ module: reqModel.analysisContext.applicationModule,
1144
+ variant: reqModel.analysisContext.buildVariant,
1145
+ analysisMode: opts.analysisMode,
1146
+ },
1147
+ opts,
1148
+ };
1149
+ const results = new Map();
1150
+ await runPool(items, opts.concurrency ?? 4, async (item) => {
1151
+ let result;
1152
+ try {
1153
+ result = await renderAreaUnified(item, ctx);
1154
+ } catch (error) {
1155
+ result = {
1156
+ status: "error",
1157
+ error: error instanceof Error ? error.message : String(error),
1158
+ areaId: item.area.id,
1159
+ features: new Map(),
1160
+ attempt: 0,
1161
+ };
1162
+ await appendStatusEvent(
1163
+ opts.renderDir,
1164
+ {
1165
+ phase: "finish",
1166
+ areaId: item.area.id,
1167
+ status: result.status,
1168
+ },
1169
+ clock,
1170
+ );
1171
+ }
1172
+ results.set(item.area.id, result);
1173
+ });
1174
+ return results;
1175
+ }
1176
+ /** Absolute path to the self-check script bundled with this skill (not this
1177
+ * TS runtime's own build), resolved relative to this module rather than
1178
+ * process.cwd() so it works regardless of where `pnpm stage2` is invoked
1179
+ * from. The script lives beside the runtime directory under the skill root,
1180
+ * so it travels with the skill wherever it's installed. */
1181
+ export function defaultCheckScriptPath() {
1182
+ return path.resolve(
1183
+ path.dirname(fileURLToPath(import.meta.url)),
1184
+ "..",
1185
+ "..",
1186
+ "..",
1187
+ "reqgen-selfcheck.mjs",
1188
+ );
1189
+ }
1190
+ export async function prepareFeatureInputs(items, reqModel, opts) {
1191
+ await mkdir(opts.renderDir, { recursive: true });
1192
+ const promptContext = {
1193
+ projectRoot: opts.projectRoot,
1194
+ module: reqModel.analysisContext.applicationModule,
1195
+ variant: reqModel.analysisContext.buildVariant,
1196
+ analysisMode: opts.analysisMode,
1197
+ };
1198
+ const checkScriptPath = defaultCheckScriptPath();
1199
+ const entries = [];
1200
+ for (const item of items) {
1201
+ const dir = path.join(opts.renderDir, renderDirFor(item.feature.id));
1202
+ await mkdir(dir, { recursive: true });
1203
+ const paths = {
1204
+ expectedPath: path.join(dir, "expected.json"),
1205
+ outputPath: path.join(dir, "output.json"),
1206
+ checkScriptPath,
1207
+ };
1208
+ const taskMd = buildTaskMdForSubagent(
1209
+ item,
1210
+ reqModel,
1211
+ promptContext,
1212
+ opts.gapAuditLinks,
1213
+ paths,
1214
+ opts.nameTrace,
1215
+ );
1216
+ const taskPath = path.join(dir, "task.md");
1217
+ await writeFile(taskPath, taskMd, "utf8");
1218
+ await writeFile(
1219
+ paths.expectedPath,
1220
+ JSON.stringify(buildFeatureExpected(item, opts.nameTrace), null, 2),
1221
+ "utf8",
1222
+ );
1223
+ entries.push({
1224
+ id: item.feature.id,
1225
+ taskPath,
1226
+ expectedPath: paths.expectedPath,
1227
+ outputPath: paths.outputPath,
1228
+ });
1229
+ }
1230
+ return entries;
1231
+ }
1232
+ export async function prepareAreaInputs(items, reqModel, opts) {
1233
+ await mkdir(opts.renderDir, { recursive: true });
1234
+ const promptContext = {
1235
+ projectRoot: opts.projectRoot,
1236
+ module: reqModel.analysisContext.applicationModule,
1237
+ variant: reqModel.analysisContext.buildVariant,
1238
+ analysisMode: opts.analysisMode,
1239
+ };
1240
+ const checkScriptPath = defaultCheckScriptPath();
1241
+ const entries = [];
1242
+ for (const item of items) {
1243
+ const dir = path.join(opts.renderDir, areaDirFor(item.area.id));
1244
+ await mkdir(dir, { recursive: true });
1245
+ const paths = {
1246
+ expectedPath: path.join(dir, "expected.json"),
1247
+ outputPath: path.join(dir, "output.json"),
1248
+ checkScriptPath,
1249
+ };
1250
+ const taskMd = buildAreaTaskMdForSubagent(
1251
+ item,
1252
+ reqModel,
1253
+ promptContext,
1254
+ opts.gapAuditLinks,
1255
+ paths,
1256
+ opts.nameTrace,
1257
+ );
1258
+ const taskPath = path.join(dir, "task.md");
1259
+ await writeFile(taskPath, taskMd, "utf8");
1260
+ await writeFile(
1261
+ paths.expectedPath,
1262
+ JSON.stringify(buildAreaExpected(item, opts.nameTrace), null, 2),
1263
+ "utf8",
1264
+ );
1265
+ entries.push({
1266
+ id: item.area.id,
1267
+ taskPath,
1268
+ expectedPath: paths.expectedPath,
1269
+ outputPath: paths.outputPath,
1270
+ });
1271
+ }
1272
+ return entries;
1273
+ }
1274
+ async function isUnitDone(outputPath) {
1275
+ try {
1276
+ const parsed = JSON.parse(await readFile(outputPath, "utf8"));
1277
+ // finalized, not just status === "ok": in subagent mode outputPath is
1278
+ // written once by the subagent's own raw (pre-check) judgment and then
1279
+ // overwritten in place by `finalize` (ADR-0005) — a raw draft can
1280
+ // legitimately have status "ok" (the subagent's own self-assessment)
1281
+ // before it has actually passed self-check, so status alone would
1282
+ // mark an interrupted, never-finalized unit as done.
1283
+ return parsed.finalized === true && parsed.status === "ok";
1284
+ } catch {
1285
+ return false;
1286
+ }
1287
+ }
1288
+ /** Safe to call every time prepare runs, including on resume: it always
1289
+ * reflects which output.json files exist *right now*, never what a prior
1290
+ * dispatch session merely believed it had finished — so a fresh session
1291
+ * (with no memory of the last one) can trust it as ground truth rather than
1292
+ * trusting its own or another session's bookkeeping. */
1293
+ export async function writeRenderList(
1294
+ outputDirectory,
1295
+ checkScriptPath,
1296
+ entries,
1297
+ ) {
1298
+ const doneFlags = await Promise.all(
1299
+ entries.map((entry) => isUnitDone(entry.outputPath)),
1300
+ );
1301
+ const lines = entries.map(
1302
+ (entry, index) => `- [${doneFlags[index] ? "x" : " "}] ${entry.id}`,
1303
+ );
1304
+ const listPath = path.join(outputDirectory, "render-list.md");
1305
+ await writeFile(
1306
+ listPath,
1307
+ `${lines.length === 0 ? "" : `${lines.join("\n")}\n`}`,
1308
+ "utf8",
1309
+ );
1310
+ const manifestPath = path.join(outputDirectory, "render-manifest.json");
1311
+ await writeFile(
1312
+ manifestPath,
1313
+ JSON.stringify({ checkScriptPath, entries }, null, 2),
1314
+ "utf8",
1315
+ );
1316
+ const done = doneFlags.filter(Boolean).length;
1317
+ return {
1318
+ listPath,
1319
+ manifestPath,
1320
+ total: entries.length,
1321
+ done,
1322
+ pending: entries.length - done,
1323
+ };
1324
+ }