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.
- package/agents/api-analyzer.md +113 -0
- package/agents/app-action.md +122 -0
- package/agents/code-reviewer.md +191 -295
- package/agents/coder.md +377 -0
- package/agents/self-tester.md +8 -8
- package/agents/spec-designer.md +56 -0
- package/package.json +1 -1
- package/skills/a2h-app-req-gen/SKILL.md +174 -0
- package/skills/a2h-app-req-gen/references/spec-handoff.md +99 -0
- package/skills/a2h-app-req-gen/references/subagent-render.md +115 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/build.gradle.kts +41 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradle.properties +3 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew +251 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/gradlew.bat +94 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/settings.gradle.kts +17 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawAnalysisDetector.java +1911 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawIssueRegistry.java +37 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawJson.java +146 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawLocation.java +96 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/java/dev/reqgen/analyzer/jvm/RawRecordWriter.java +227 -0
- 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
- package/skills/a2h-app-req-gen/scripts/reqgen/analyzer-jvm/src/main/resources/dev/reqgen/analyzer/jvm/raw-record-v1.schema.json +93 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/assets/dashboard.html +151 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-lint.init.gradle +60 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/gradle/reqgen-model.init.gradle +204 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/package.json +14 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/backend.js +323 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/cli.js +811 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/gap-audit-link.js +62 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/index.js +66 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/merge.js +605 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/name-trace.js +153 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/orchestrate.js +1324 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/output-parse.js +50 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/overview.js +10 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/pool.js +23 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/post-merge-fix.js +146 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt-loader.js +51 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompt.js +967 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/prompts.md +301 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/serve.js +78 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/short-id.js +46 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/snapshot.js +267 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/agent-render/validate.js +122 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/callback-call-sites.js +62 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/build.js +1291 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/gap-audit/index.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/area-ownership.js +392 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/build.js +1995 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/callback-flow.js +277 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/component-variants.js +725 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/graph/index.js +15 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/index.js +12 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/policy.js +469 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/names/traceability.js +468 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/build.js +1066 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/req-model/index.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android-interaction-registry.js +174 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/android.js +1062 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/area-titles.js +678 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/builder.js +350 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-api-registry.js +395 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose-call-sites.js +2008 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/compose.js +9059 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/dynamic-repeated-call-sites.js +529 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/index.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/interaction-effects.js +1149 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/jvm.js +6272 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/normalize.js +33 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/pending-intents.js +565 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/resource-id.js +42 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/route-state-variants.js +781 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/state.js +234 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/analysis/source-facts/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/extract.js +40 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/index.js +6 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/manifest.js +513 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/resources.js +398 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/views.js +447 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/android/xml-parser.js +175 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/cli.js +398 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/area-graph.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/artifact.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/common.js +16 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/diagnostic.js +10 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/gap-audit.js +14 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/index.js +11 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/name-traceability.js +26 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/raw-fact.js +40 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/req-model.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/run.js +19 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/semantic-completeness.js +11 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/contracts/source-facts.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/core/output-guard.js +41 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/core/stable-id.js +92 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/dashboard-cli.js +169 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/diagnostics.js +104 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/dump.js +1028 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/dump/index.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/dump-cli.js +251 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/artifacts.js +49 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/index.js +18 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/quality-report.js +125 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/render.js +353 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/generation/types.js +2 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/graph/paths.js +474 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/index.js +15 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/protocol.js +408 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/jvm/runner.js +452 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/index.js +8 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/output.js +165 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/pipeline/run-stage1.js +891 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/discovery.js +299 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-model.js +358 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/gradle-runner.js +165 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/resource-profile.js +60 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/project/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff-cli.js +151 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/spec-handoff.js +1004 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-cli.js +109 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-input.js +419 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/stage2-name-trace-input.js +95 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/area-graph.js +444 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/index.js +7 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-model.js +580 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/req-text.js +195 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/semantic-area-graph.js +1431 -0
- package/skills/a2h-app-req-gen/scripts/reqgen/src/validation/types.js +1 -0
- package/skills/a2h-app-req-gen/scripts/reqgen-audit.mjs +298 -0
- package/skills/a2h-app-req-gen/scripts/reqgen-selfcheck.mjs +555 -0
- package/skills/a2h-code-review/SKILL.md +379 -0
- package/skills/{hmos-integration-test → a2h-integration-test}/README.md +6 -6
- package/skills/{hmos-integration-test → a2h-integration-test}/SKILL.md +6 -6
- package/skills/{hmos-integration-test → a2h-integration-test}/scripts/report-tool.mjs +1 -1
- package/skills/a2h-spec-design/SKILL.md +330 -0
- package/skills/a2h-spec-design/scripts/inspect_spec_design_inputs.mjs +207 -0
- package/skills/a2h-spec-design/scripts/move_spec_technical_reference.mjs +198 -0
- package/skills/a2h-spec-design/scripts/validate_spec_design_output.mjs +256 -0
- package/skills/a2h-spec-generate/SKILL.md +696 -0
- package/skills/a2h-spec-generate/references/trace-template.md +115 -0
- package/skills/a2h-spec-generate/scripts/parse_requirements.mjs +537 -0
- package/skills/a2h-spec-generate/template/REQ.txt +22 -0
- package/skills/a2h-spec-generate/template/REQ.xlsx +0 -0
- package/skills/a2h-spec-implement/SKILL.md +548 -0
- package/skills/a2h-spec-implement/references/harmony-pitfalls.md +186 -0
- package/skills/a2h-spec-implement/references/l1-unit-test.md +185 -0
- package/skills/a2h-spec-implement/references/l3-e2e-uitest.md +167 -0
- package/skills/a2h-spec-implement/references/test-core.md +132 -0
- package/skills/a2h-spec-implement/rules/arkts/arkts-standards.md +184 -0
- package/skills/a2h-spec-implement/rules/arkts/conventions/coding-style.md +96 -0
- package/skills/a2h-spec-implement/rules/arkts/conventions/security.md +126 -0
- package/skills/a2h-spec-implement/rules/arkts/language/arkts-rules.md +650 -0
- package/skills/a2h-spec-implement/rules/arkts/language/arkui-structure-rules.md +173 -0
- package/skills/a2h-spec-implement/rules/arkts/language/ts-to-arkts-rewrites.md +58 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/skills/a2h-spec-implement/rules/arkts/ui/component-cookbook.md +431 -0
- package/skills/a2h-spec-implement/rules/arkts/ui/state-management.md +152 -0
- package/skills/a2h-spec-implement/rules/arkts/ui/ui-quality.md +67 -0
- package/skills/a2h-spec-implement/rules/arkts/ui/ui-runtime-diagnosis.md +32 -0
- package/skills/a2h-spec-implement/rules/pipeline/build/package-set-collection.md +43 -0
- package/skills/a2h-spec-implement/scripts/_lib/common.mjs +97 -0
- package/skills/a2h-spec-implement/scripts/_lib/config.mjs +70 -0
- package/skills/a2h-spec-implement/scripts/_lib/lessons_cli.mjs +48 -0
- package/skills/a2h-spec-implement/scripts/build_lessons.mjs +24 -0
- package/skills/a2h-spec-implement/scripts/code_pattern_lessons.mjs +26 -0
- package/skills/a2h-spec-implement/scripts/device_ui.mjs +433 -0
- package/skills/a2h-spec-implement/scripts/device_ui_core.mjs +895 -0
- package/skills/a2h-spec-implement/scripts/ledger/aggregate_run.mjs +207 -0
- package/skills/a2h-spec-implement/scripts/manifest/emit_summary_line.mjs +37 -0
- package/skills/a2h-spec-implement/scripts/manifest/finalize_run.mjs +115 -0
- package/skills/a2h-spec-implement/scripts/manifest/render_final_summary.mjs +78 -0
- package/skills/a2h-spec-implement/scripts/manifest/root_mirror.mjs +137 -0
- package/skills/a2h-spec-implement/scripts/package.json +12 -0
- package/skills/a2h-spec-implement/scripts/state_propagation_check.mjs +676 -0
- package/skills/a2h-spec-implement/scripts/test/l2_run.mjs +244 -0
- package/skills/a2h-spec-implement/scripts/test/tp_coverage.mjs +231 -0
- package/skills/a2h-spec-implement/scripts/ui_memory.mjs +161 -0
- package/skills/a2h-spec-implement/scripts/utils/ensure_app.mjs +134 -0
- package/skills/a2h-ui-transfer/SKILL.md +20 -9
- package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +80 -0
- package/skills/hmos-convert-pipeline/SKILL.md +7 -5
- package/skills/hmos-incremental-ui-align/README.md +7 -7
- package/skills/hmos-incremental-ui-align/SKILL.md +1 -1
- package/skills/hmos-incremental-ui-align/page_align.md +1 -1
- package/skills/hmos-spec-generate/SKILL.md +2 -2
- package/skills/hmos-spec-generate/scripts/parse_requirements.mjs +537 -0
- package/skills/hmos-fix-build-errors/SKILL.md +0 -266
- package/skills/hmos-fix-build-errors/references/arkts-strict-patterns.md +0 -219
- package/skills/hmos-fix-build-errors/references/known-patterns.md +0 -157
- package/skills/hmos-fix-build-errors/references/rdb-entity-pattern.md +0 -131
- package/skills/hmos-spec-generate/scripts/parse_requirements.ts +0 -515
- /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/resolve-metadata-tool.mjs +0 -0
- /package/skills/{hmos-integration-test → a2h-integration-test}/scripts/self-test-runner.mjs +0 -0
- /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
|
+
}
|