@mmerterden/multi-agent-pipeline 8.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (817) hide show
  1. package/CHANGELOG.md +2623 -0
  2. package/LICENSE +21 -0
  3. package/README.md +852 -0
  4. package/docs/FIGMA_PIPELINE.md +138 -0
  5. package/docs/GENERICITY-REVIEW.md +277 -0
  6. package/docs/STABILITY-FIX-PLAN.md +168 -0
  7. package/docs/adr/0001-three-model-triage.md +81 -0
  8. package/docs/adr/0002-instruction-driven-flag.md +62 -0
  9. package/docs/adr/0003-unified-shared-skills.md +55 -0
  10. package/docs/adr/0004-zero-dependency-philosophy.md +60 -0
  11. package/docs/adr/0005-lazy-phase-docs.md +68 -0
  12. package/docs/adr/0006-skills-core-external-split.md +52 -0
  13. package/docs/adr/0007-multi-tool-adapter-framework.md +110 -0
  14. package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +98 -0
  15. package/docs/adr/README.md +33 -0
  16. package/docs/architecture.md +181 -0
  17. package/docs/best-practices.md +93 -0
  18. package/docs/features.md +274 -0
  19. package/docs/performance.md +116 -0
  20. package/docs/recovery-guide.md +479 -0
  21. package/index.js +76 -0
  22. package/install/_adapters.mjs +69 -0
  23. package/install/_common.mjs +150 -0
  24. package/install/_copilot-instructions.mjs +32 -0
  25. package/install/_dev-only-files.mjs +23 -0
  26. package/install/_platform-filter.mjs +132 -0
  27. package/install/_telemetry.mjs +79 -0
  28. package/install/claude.mjs +332 -0
  29. package/install/copilot.mjs +254 -0
  30. package/install/index.mjs +179 -0
  31. package/install/templates/copilot-instructions.md +319 -0
  32. package/install.js +24 -0
  33. package/package.json +78 -0
  34. package/pipeline/adapters/_base.mjs +288 -0
  35. package/pipeline/adapters/copilot-chat.mjs +158 -0
  36. package/pipeline/adapters/cursor.mjs +187 -0
  37. package/pipeline/agents/android-architect.md +42 -0
  38. package/pipeline/agents/backend-architect.md +43 -0
  39. package/pipeline/agents/code-reviewer.md +57 -0
  40. package/pipeline/agents/dev-critic.md +148 -0
  41. package/pipeline/agents/explorer.md +34 -0
  42. package/pipeline/agents/ios-architect.md +41 -0
  43. package/pipeline/agents/security-auditor.md +98 -0
  44. package/pipeline/agents/task-clarifier.md +113 -0
  45. package/pipeline/claude-md-template.md +55 -0
  46. package/pipeline/commands/archive-guard.md +45 -0
  47. package/pipeline/commands/deploy.md +54 -0
  48. package/pipeline/commands/figma-to-swiftui.md +295 -0
  49. package/pipeline/commands/multi-agent/_account-picker.md +90 -0
  50. package/pipeline/commands/multi-agent/_dev-context.md +111 -0
  51. package/pipeline/commands/multi-agent/_input-parser.md +43 -0
  52. package/pipeline/commands/multi-agent/_repo-picker.md +76 -0
  53. package/pipeline/commands/multi-agent/autopilot.md +116 -0
  54. package/pipeline/commands/multi-agent/channels.md +465 -0
  55. package/pipeline/commands/multi-agent/delete.md +66 -0
  56. package/pipeline/commands/multi-agent/dev-autopilot.md +120 -0
  57. package/pipeline/commands/multi-agent/dev-local-autopilot.md +110 -0
  58. package/pipeline/commands/multi-agent/dev-local.md +105 -0
  59. package/pipeline/commands/multi-agent/dev.md +246 -0
  60. package/pipeline/commands/multi-agent/diff-explain.md +68 -0
  61. package/pipeline/commands/multi-agent/help.md +422 -0
  62. package/pipeline/commands/multi-agent/issue.md +79 -0
  63. package/pipeline/commands/multi-agent/jira.md +132 -0
  64. package/pipeline/commands/multi-agent/kill.md +38 -0
  65. package/pipeline/commands/multi-agent/language.md +94 -0
  66. package/pipeline/commands/multi-agent/local-autopilot.md +139 -0
  67. package/pipeline/commands/multi-agent/local.md +117 -0
  68. package/pipeline/commands/multi-agent/log.md +25 -0
  69. package/pipeline/commands/multi-agent/manual-test.md +43 -0
  70. package/pipeline/commands/multi-agent/purge.md +39 -0
  71. package/pipeline/commands/multi-agent/refactor.md +188 -0
  72. package/pipeline/commands/multi-agent/refs/android-guide.md +250 -0
  73. package/pipeline/commands/multi-agent/refs/audit-guide.md +240 -0
  74. package/pipeline/commands/multi-agent/refs/backend-guide.md +135 -0
  75. package/pipeline/commands/multi-agent/refs/channels/confluence.md +153 -0
  76. package/pipeline/commands/multi-agent/refs/channels/issue-comment.md +141 -0
  77. package/pipeline/commands/multi-agent/refs/channels/jira.md +127 -0
  78. package/pipeline/commands/multi-agent/refs/channels/pr-review-actions.md +135 -0
  79. package/pipeline/commands/multi-agent/refs/channels/pr.md +139 -0
  80. package/pipeline/commands/multi-agent/refs/channels/wiki.md +66 -0
  81. package/pipeline/commands/multi-agent/refs/component-dispatch.md +92 -0
  82. package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +326 -0
  83. package/pipeline/commands/multi-agent/refs/frontend-guide.md +136 -0
  84. package/pipeline/commands/multi-agent/refs/issue-jira-triad.md +104 -0
  85. package/pipeline/commands/multi-agent/refs/keychain.md +80 -0
  86. package/pipeline/commands/multi-agent/refs/knowledge.md +112 -0
  87. package/pipeline/commands/multi-agent/refs/multi-repo-integration-build.md +207 -0
  88. package/pipeline/commands/multi-agent/refs/phases/log-format.md +89 -0
  89. package/pipeline/commands/multi-agent/refs/phases/modes.md +156 -0
  90. package/pipeline/commands/multi-agent/refs/phases/operations.md +91 -0
  91. package/pipeline/commands/multi-agent/refs/phases/phase-0-init.md +481 -0
  92. package/pipeline/commands/multi-agent/refs/phases/phase-1-analysis.md +264 -0
  93. package/pipeline/commands/multi-agent/refs/phases/phase-2-planning.md +278 -0
  94. package/pipeline/commands/multi-agent/refs/phases/phase-3-dev.md +364 -0
  95. package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +378 -0
  96. package/pipeline/commands/multi-agent/refs/phases/phase-5-test.md +129 -0
  97. package/pipeline/commands/multi-agent/refs/phases/phase-6-commit.md +339 -0
  98. package/pipeline/commands/multi-agent/refs/phases/phase-7-report.md +361 -0
  99. package/pipeline/commands/multi-agent/refs/phases.md +187 -0
  100. package/pipeline/commands/multi-agent/refs/progress-contract.md +155 -0
  101. package/pipeline/commands/multi-agent/refs/rules.md +189 -0
  102. package/pipeline/commands/multi-agent/refs/swiftui-guide.md +254 -0
  103. package/pipeline/commands/multi-agent/refs/tracker-contract.md +256 -0
  104. package/pipeline/commands/multi-agent/refs/wiki-capture.md +109 -0
  105. package/pipeline/commands/multi-agent/resume.md +28 -0
  106. package/pipeline/commands/multi-agent/review.md +228 -0
  107. package/pipeline/commands/multi-agent/scan.md +74 -0
  108. package/pipeline/commands/multi-agent/search.md +97 -0
  109. package/pipeline/commands/multi-agent/setup.md +767 -0
  110. package/pipeline/commands/multi-agent/stack.md +48 -0
  111. package/pipeline/commands/multi-agent/status.md +38 -0
  112. package/pipeline/commands/multi-agent/sync.md +319 -0
  113. package/pipeline/commands/multi-agent/test.md +39 -0
  114. package/pipeline/commands/multi-agent/update.md +88 -0
  115. package/pipeline/commands/multi-agent.md +293 -0
  116. package/pipeline/commands/security-review.md +6 -0
  117. package/pipeline/commands/sim-test.md +256 -0
  118. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-1-analysis.json +25 -0
  119. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-2-plan.json +30 -0
  120. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-review.json +20 -0
  121. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-triage.json +15 -0
  122. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/metadata.json +14 -0
  123. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/task.json +12 -0
  124. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-1-analysis.json +29 -0
  125. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-2-plan.json +43 -0
  126. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-review.json +35 -0
  127. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-triage.json +35 -0
  128. package/pipeline/eval/golden-tasks/02-android-feature-compose/metadata.json +14 -0
  129. package/pipeline/eval/golden-tasks/02-android-feature-compose/task.json +12 -0
  130. package/pipeline/eval/golden-tasks/README.md +65 -0
  131. package/pipeline/eval/triage/01-empty-findings/expected.json +6 -0
  132. package/pipeline/eval/triage/01-empty-findings/input.json +5 -0
  133. package/pipeline/eval/triage/01-empty-findings/notes.md +7 -0
  134. package/pipeline/eval/triage/02-real-blocker/expected.json +15 -0
  135. package/pipeline/eval/triage/02-real-blocker/input.json +14 -0
  136. package/pipeline/eval/triage/02-real-blocker/notes.md +7 -0
  137. package/pipeline/eval/triage/03-out-of-scope-defer/expected.json +18 -0
  138. package/pipeline/eval/triage/03-out-of-scope-defer/input.json +14 -0
  139. package/pipeline/eval/triage/03-out-of-scope-defer/notes.md +10 -0
  140. package/pipeline/eval/triage/04-false-positive-reject/expected.json +18 -0
  141. package/pipeline/eval/triage/04-false-positive-reject/input.json +14 -0
  142. package/pipeline/eval/triage/04-false-positive-reject/notes.md +10 -0
  143. package/pipeline/eval/triage/05-mixed-classification/expected.json +43 -0
  144. package/pipeline/eval/triage/05-mixed-classification/input.json +38 -0
  145. package/pipeline/eval/triage/05-mixed-classification/notes.md +17 -0
  146. package/pipeline/eval/triage/06-severity-mismatch/expected.json +15 -0
  147. package/pipeline/eval/triage/06-severity-mismatch/input.json +14 -0
  148. package/pipeline/eval/triage/06-severity-mismatch/notes.md +9 -0
  149. package/pipeline/eval/triage/07-duplicate-reviewers/expected.json +27 -0
  150. package/pipeline/eval/triage/07-duplicate-reviewers/input.json +22 -0
  151. package/pipeline/eval/triage/07-duplicate-reviewers/notes.md +9 -0
  152. package/pipeline/eval/triage/08-style-misclassified/expected.json +18 -0
  153. package/pipeline/eval/triage/08-style-misclassified/input.json +14 -0
  154. package/pipeline/eval/triage/08-style-misclassified/notes.md +9 -0
  155. package/pipeline/eval/triage/09-cascading-finding/expected.json +23 -0
  156. package/pipeline/eval/triage/09-cascading-finding/input.json +22 -0
  157. package/pipeline/eval/triage/09-cascading-finding/notes.md +9 -0
  158. package/pipeline/eval/triage/10-deferred-crossref/expected.json +18 -0
  159. package/pipeline/eval/triage/10-deferred-crossref/input.json +14 -0
  160. package/pipeline/eval/triage/10-deferred-crossref/notes.md +9 -0
  161. package/pipeline/eval/triage/11-vercel-token-leak-blocker/expected.json +27 -0
  162. package/pipeline/eval/triage/11-vercel-token-leak-blocker/input.json +22 -0
  163. package/pipeline/eval/triage/11-vercel-token-leak-blocker/notes.md +14 -0
  164. package/pipeline/eval/triage/README.md +54 -0
  165. package/pipeline/lib/account-resolver.sh +204 -0
  166. package/pipeline/lib/channels-multi-repo.sh +218 -0
  167. package/pipeline/lib/context-link-extractor.sh +192 -0
  168. package/pipeline/lib/credential-store-resolver.sh +57 -0
  169. package/pipeline/lib/credential-store.sh +226 -0
  170. package/pipeline/lib/fetch-confluence.sh +358 -0
  171. package/pipeline/lib/fetch-crashlytics.sh +314 -0
  172. package/pipeline/lib/fetch-fortify.sh +321 -0
  173. package/pipeline/lib/fetch-swagger.sh +270 -0
  174. package/pipeline/lib/issue-fetcher.sh +333 -0
  175. package/pipeline/lib/multi-repo-pipeline.sh +252 -0
  176. package/pipeline/lib/plan-todos.sh +284 -0
  177. package/pipeline/lib/post-pr-review.sh +374 -0
  178. package/pipeline/lib/repo-cache.sh +231 -0
  179. package/pipeline/lib/review-watch.sh +244 -0
  180. package/pipeline/lib/shadow-git.sh +222 -0
  181. package/pipeline/lib/submodule-detector.sh +177 -0
  182. package/pipeline/lib/vercel-deploy.sh +170 -0
  183. package/pipeline/preferences-template.json +132 -0
  184. package/pipeline/rules/app-store-guidelines.md +59 -0
  185. package/pipeline/rules/code-review.md +27 -0
  186. package/pipeline/rules/code-style.md +37 -0
  187. package/pipeline/rules/debugging.md +24 -0
  188. package/pipeline/rules/figma-pipeline.md +190 -0
  189. package/pipeline/rules/git-conventions.md +29 -0
  190. package/pipeline/rules/kotlin-android.md +92 -0
  191. package/pipeline/rules/performance.md +23 -0
  192. package/pipeline/rules/security.md +39 -0
  193. package/pipeline/rules/swiftui-qa.md +32 -0
  194. package/pipeline/rules/tdd.md +25 -0
  195. package/pipeline/rules/testing.md +37 -0
  196. package/pipeline/schemas/agent-state.schema.json +273 -0
  197. package/pipeline/schemas/analysis-output.schema.json +59 -0
  198. package/pipeline/schemas/clarify-output.schema.json +74 -0
  199. package/pipeline/schemas/dev-critic-output.schema.json +104 -0
  200. package/pipeline/schemas/diff-risk.schema.json +78 -0
  201. package/pipeline/schemas/figma-project-config.schema.json +372 -0
  202. package/pipeline/schemas/migrations/README.md +73 -0
  203. package/pipeline/schemas/migrations/figma-config-1.0.0-to-2.0.0.mjs +112 -0
  204. package/pipeline/schemas/migrations/prefs-2.0.0-to-2.1.0.mjs +75 -0
  205. package/pipeline/schemas/migrations/prefs-2.1.0-to-2.2.0.mjs +64 -0
  206. package/pipeline/schemas/migrations/prefs-2.2.0-to-2.3.0.mjs +36 -0
  207. package/pipeline/schemas/migrations/state-2.0.0-to-2.1.0.mjs +34 -0
  208. package/pipeline/schemas/plan-todos.schema.json +62 -0
  209. package/pipeline/schemas/planning-output.schema.json +57 -0
  210. package/pipeline/schemas/prefs.schema.json +1137 -0
  211. package/pipeline/schemas/reviewer-output.schema.json +55 -0
  212. package/pipeline/schemas/test-gap.schema.json +64 -0
  213. package/pipeline/schemas/token-budget.json +17 -0
  214. package/pipeline/schemas/triage-corpus.schema.json +31 -0
  215. package/pipeline/schemas/triage-output.schema.json +115 -0
  216. package/pipeline/scripts/.last-figma-sync-plan.json +23 -0
  217. package/pipeline/scripts/README-figma-smokes.md +34 -0
  218. package/pipeline/scripts/README.md +104 -0
  219. package/pipeline/scripts/aggregate-metrics.mjs +310 -0
  220. package/pipeline/scripts/audit-log-rotate.sh +61 -0
  221. package/pipeline/scripts/audit-log.sh +69 -0
  222. package/pipeline/scripts/benchmark-phase-0.sh +128 -0
  223. package/pipeline/scripts/build-skills-index.mjs +139 -0
  224. package/pipeline/scripts/classify-plan-safety.mjs +177 -0
  225. package/pipeline/scripts/cost-table.json +27 -0
  226. package/pipeline/scripts/diff-explain.mjs +276 -0
  227. package/pipeline/scripts/diff-risk-score.mjs +328 -0
  228. package/pipeline/scripts/eval-golden-tasks-live.mjs +294 -0
  229. package/pipeline/scripts/eval-golden-tasks.mjs +223 -0
  230. package/pipeline/scripts/eval-triage.mjs +171 -0
  231. package/pipeline/scripts/figma-placeholder-map.json +191 -0
  232. package/pipeline/scripts/fixtures/diff-risk-android.diff +40 -0
  233. package/pipeline/scripts/fixtures/diff-risk-ios.diff +48 -0
  234. package/pipeline/scripts/fixtures/install-layout.tsv +16 -0
  235. package/pipeline/scripts/fixtures/test-gap-node.diff +30 -0
  236. package/pipeline/scripts/fixtures/test-gap-python.diff +32 -0
  237. package/pipeline/scripts/gen-mode-dispatch.mjs +170 -0
  238. package/pipeline/scripts/gen-skills-index.mjs +90 -0
  239. package/pipeline/scripts/github-ssh-setup.sh +103 -0
  240. package/pipeline/scripts/import-figma-skills.sh +253 -0
  241. package/pipeline/scripts/keychain-save.sh +74 -0
  242. package/pipeline/scripts/keychain.py +294 -0
  243. package/pipeline/scripts/log-metric.sh +98 -0
  244. package/pipeline/scripts/match-skills.mjs +167 -0
  245. package/pipeline/scripts/memory-load.sh +46 -0
  246. package/pipeline/scripts/memory-save.sh +76 -0
  247. package/pipeline/scripts/migrate-prefs.mjs +390 -0
  248. package/pipeline/scripts/migrate-state.mjs +215 -0
  249. package/pipeline/scripts/output-quality-check.sh +125 -0
  250. package/pipeline/scripts/phase-banner.sh +158 -0
  251. package/pipeline/scripts/phase-tracker.sh +548 -0
  252. package/pipeline/scripts/pre-commit-check.sh +69 -0
  253. package/pipeline/scripts/pre-push-check.sh +77 -0
  254. package/pipeline/scripts/render-agent-log-cost.sh +149 -0
  255. package/pipeline/scripts/render-cost-summary.sh +137 -0
  256. package/pipeline/scripts/render-work-summary.sh +195 -0
  257. package/pipeline/scripts/repo-map.mjs +367 -0
  258. package/pipeline/scripts/run-aggregator.mjs +298 -0
  259. package/pipeline/scripts/scan-skills.sh +332 -0
  260. package/pipeline/scripts/search-logs.sh +291 -0
  261. package/pipeline/scripts/sign-skills.sh +67 -0
  262. package/pipeline/scripts/smoke-adapters.sh +207 -0
  263. package/pipeline/scripts/smoke-add-detail.sh +137 -0
  264. package/pipeline/scripts/smoke-agent-log-cost.sh +183 -0
  265. package/pipeline/scripts/smoke-agent-model-routing.sh +87 -0
  266. package/pipeline/scripts/smoke-bitbucket-contract.sh +223 -0
  267. package/pipeline/scripts/smoke-channels-flow.sh +130 -0
  268. package/pipeline/scripts/smoke-ci-workflows.sh +88 -0
  269. package/pipeline/scripts/smoke-clarify.sh +148 -0
  270. package/pipeline/scripts/smoke-commands-skills-parity.sh +87 -0
  271. package/pipeline/scripts/smoke-compliance-skills.sh +119 -0
  272. package/pipeline/scripts/smoke-cost-summary.sh +139 -0
  273. package/pipeline/scripts/smoke-cross-cli-behavior.sh +198 -0
  274. package/pipeline/scripts/smoke-cross-phase-cohesion.sh +128 -0
  275. package/pipeline/scripts/smoke-delete-flow.sh +151 -0
  276. package/pipeline/scripts/smoke-dev-critic.sh +144 -0
  277. package/pipeline/scripts/smoke-diff-explain.sh +128 -0
  278. package/pipeline/scripts/smoke-diff-risk.sh +161 -0
  279. package/pipeline/scripts/smoke-dynamic-skill-loading.sh +160 -0
  280. package/pipeline/scripts/smoke-eval-live.sh +136 -0
  281. package/pipeline/scripts/smoke-existing-discovery-gate.sh +71 -0
  282. package/pipeline/scripts/smoke-figma-android-parity.sh +148 -0
  283. package/pipeline/scripts/smoke-figma-config-schema.sh +144 -0
  284. package/pipeline/scripts/smoke-figma-credential-store.sh +105 -0
  285. package/pipeline/scripts/smoke-figma-cross-cli-inventory.sh +177 -0
  286. package/pipeline/scripts/smoke-figma-dispatch.sh +123 -0
  287. package/pipeline/scripts/smoke-figma-skill-import.sh +174 -0
  288. package/pipeline/scripts/smoke-figma-sync.sh +149 -0
  289. package/pipeline/scripts/smoke-identity-isolation.sh +70 -0
  290. package/pipeline/scripts/smoke-install-layout.sh +241 -0
  291. package/pipeline/scripts/smoke-install-leak-gate.sh +125 -0
  292. package/pipeline/scripts/smoke-issue-comment-template.sh +86 -0
  293. package/pipeline/scripts/smoke-issue-jira-triad.sh +120 -0
  294. package/pipeline/scripts/smoke-keychain.sh +158 -0
  295. package/pipeline/scripts/smoke-language-axis.sh +109 -0
  296. package/pipeline/scripts/smoke-lib-scripts.sh +395 -0
  297. package/pipeline/scripts/smoke-migrate-state.sh +102 -0
  298. package/pipeline/scripts/smoke-mode-dispatch-drift.sh +158 -0
  299. package/pipeline/scripts/smoke-multi-repo-integration.sh +116 -0
  300. package/pipeline/scripts/smoke-multi-repo-worktree.sh +61 -0
  301. package/pipeline/scripts/smoke-no-token-prompt.sh +69 -0
  302. package/pipeline/scripts/smoke-pat-audit.sh +107 -0
  303. package/pipeline/scripts/smoke-per-repo-memory.sh +156 -0
  304. package/pipeline/scripts/smoke-personal-data.sh +82 -0
  305. package/pipeline/scripts/smoke-phase-0-multi-repo.sh +170 -0
  306. package/pipeline/scripts/smoke-phase-6-multi.sh +79 -0
  307. package/pipeline/scripts/smoke-phase-banner.sh +101 -0
  308. package/pipeline/scripts/smoke-phase-tracker.sh +255 -0
  309. package/pipeline/scripts/smoke-phase0-bridge-contract.sh +241 -0
  310. package/pipeline/scripts/smoke-phase4-triage.sh +142 -0
  311. package/pipeline/scripts/smoke-plan-approval-gate.sh +71 -0
  312. package/pipeline/scripts/smoke-plan-safety.sh +139 -0
  313. package/pipeline/scripts/smoke-plan-todos.sh +193 -0
  314. package/pipeline/scripts/smoke-pr-review-actions.sh +152 -0
  315. package/pipeline/scripts/smoke-pre-commit.sh +138 -0
  316. package/pipeline/scripts/smoke-pref-migration.sh +224 -0
  317. package/pipeline/scripts/smoke-prefs-language.sh +134 -0
  318. package/pipeline/scripts/smoke-progress-contract.sh +118 -0
  319. package/pipeline/scripts/smoke-push-retry.sh +75 -0
  320. package/pipeline/scripts/smoke-readme-counts.sh +120 -0
  321. package/pipeline/scripts/smoke-repo-map.sh +300 -0
  322. package/pipeline/scripts/smoke-review-watch.sh +134 -0
  323. package/pipeline/scripts/smoke-run-aggregator.sh +216 -0
  324. package/pipeline/scripts/smoke-schema-validation.sh +173 -0
  325. package/pipeline/scripts/smoke-search.sh +187 -0
  326. package/pipeline/scripts/smoke-shadow-git.sh +175 -0
  327. package/pipeline/scripts/smoke-skill-authoring.sh +142 -0
  328. package/pipeline/scripts/smoke-skill-language.sh +83 -0
  329. package/pipeline/scripts/smoke-skill-manifest.sh +138 -0
  330. package/pipeline/scripts/smoke-skill-scan.sh +198 -0
  331. package/pipeline/scripts/smoke-stack-swap.sh +132 -0
  332. package/pipeline/scripts/smoke-subagent-validators.sh +105 -0
  333. package/pipeline/scripts/smoke-sync-delegation.sh +74 -0
  334. package/pipeline/scripts/smoke-sync-parity.sh +92 -0
  335. package/pipeline/scripts/smoke-tasklist-ordering.sh +111 -0
  336. package/pipeline/scripts/smoke-telemetry.sh +147 -0
  337. package/pipeline/scripts/smoke-test-gap.sh +183 -0
  338. package/pipeline/scripts/smoke-token-budget.sh +67 -0
  339. package/pipeline/scripts/smoke-tracker-contract.sh +129 -0
  340. package/pipeline/scripts/smoke-tracker-tokens-invocation.sh +65 -0
  341. package/pipeline/scripts/smoke-triage-memory.sh +174 -0
  342. package/pipeline/scripts/smoke-url-enrichment.sh +70 -0
  343. package/pipeline/scripts/smoke-validator-contradiction.sh +67 -0
  344. package/pipeline/scripts/smoke-vercel-deploy-redact.sh +129 -0
  345. package/pipeline/scripts/smoke-wiki-integration.sh +146 -0
  346. package/pipeline/scripts/smoke-work-summary.sh +163 -0
  347. package/pipeline/scripts/smoke-worktree-path-convention.sh +86 -0
  348. package/pipeline/scripts/smoke-write-state.sh +115 -0
  349. package/pipeline/scripts/stack-swap.sh +182 -0
  350. package/pipeline/scripts/sync-figma-source.sh +228 -0
  351. package/pipeline/scripts/sync-parity-check.sh +135 -0
  352. package/pipeline/scripts/test-gap-rules/android.json +25 -0
  353. package/pipeline/scripts/test-gap-rules/ios.json +29 -0
  354. package/pipeline/scripts/test-gap-rules/node.json +17 -0
  355. package/pipeline/scripts/test-gap-rules/python.json +19 -0
  356. package/pipeline/scripts/test-gap-scan.mjs +343 -0
  357. package/pipeline/scripts/token-budget-report.mjs +145 -0
  358. package/pipeline/scripts/triage-memory.mjs +258 -0
  359. package/pipeline/scripts/ui-tree-dumper.swift +122 -0
  360. package/pipeline/scripts/uninstall.mjs +331 -0
  361. package/pipeline/scripts/update-issue-progress.sh +146 -0
  362. package/pipeline/scripts/validate-analysis.mjs +132 -0
  363. package/pipeline/scripts/validate-diff-risk.mjs +117 -0
  364. package/pipeline/scripts/validate-planning.mjs +180 -0
  365. package/pipeline/scripts/validate-reviewer.mjs +131 -0
  366. package/pipeline/scripts/validate-schemas.mjs +88 -0
  367. package/pipeline/scripts/validate-test-gap.mjs +90 -0
  368. package/pipeline/scripts/validate-triage.mjs +175 -0
  369. package/pipeline/scripts/verify-skills.sh +126 -0
  370. package/pipeline/scripts/write-state.mjs +175 -0
  371. package/pipeline/skills/.skill-manifest.json +779 -0
  372. package/pipeline/skills/.skills-index.json +1771 -0
  373. package/pipeline/skills/figma-android/README.md +36 -0
  374. package/pipeline/skills/figma-android/figma-component-code-connect/SKILL.md +62 -0
  375. package/pipeline/skills/figma-android/figma-component-implement/SKILL.md +158 -0
  376. package/pipeline/skills/figma-android/figma-component-test/SKILL.md +120 -0
  377. package/pipeline/skills/figma-android/figma-component-wiki/SKILL.md +35 -0
  378. package/pipeline/skills/figma-android/figma-to-component/SKILL.md +124 -0
  379. package/pipeline/skills/figma-common/README.md +57 -0
  380. package/pipeline/skills/figma-common/figma-cli-iterate/SKILL.md +277 -0
  381. package/pipeline/skills/figma-common/figma-cli-iterate-mend/SKILL.md +498 -0
  382. package/pipeline/skills/figma-common/figma-cli-lean-iterate/SKILL.md +283 -0
  383. package/pipeline/skills/figma-common/figma-cli-skip/SKILL.md +362 -0
  384. package/pipeline/skills/figma-common/figma-commit/COMMON_REBASE.md +206 -0
  385. package/pipeline/skills/figma-common/figma-commit/REVIEW.md +337 -0
  386. package/pipeline/skills/figma-common/figma-commit/SKILL.md +211 -0
  387. package/pipeline/skills/figma-common/figma-component-confluence-sync/SKILL.md +218 -0
  388. package/pipeline/skills/figma-common/figma-component-start/SKILL.md +246 -0
  389. package/pipeline/skills/figma-common/figma-component-status-update/SKILL.md +73 -0
  390. package/pipeline/skills/figma-common/figma-fix/SKILL.md +316 -0
  391. package/pipeline/skills/figma-common/figma-form-integration/SKILL.md +542 -0
  392. package/pipeline/skills/figma-common/figma-issue/SKILL.md +745 -0
  393. package/pipeline/skills/figma-common/figma-iterate/SKILL.md +203 -0
  394. package/pipeline/skills/figma-common/figma-iteration-commit/SKILL.md +1015 -0
  395. package/pipeline/skills/figma-common/figma-mend/SKILL.md +331 -0
  396. package/pipeline/skills/figma-common/figma-price-integration/SKILL.md +398 -0
  397. package/pipeline/skills/figma-common/figma-remote-mcp-auth/SKILL.md +104 -0
  398. package/pipeline/skills/figma-common/figma-review/SKILL.md +395 -0
  399. package/pipeline/skills/figma-common/figma-setup/SKILL.md +514 -0
  400. package/pipeline/skills/figma-common/figma-setup/scripts/fetch-mcp-token.py +592 -0
  401. package/pipeline/skills/figma-common/figma-skip/SKILL.md +129 -0
  402. package/pipeline/skills/figma-common/figma-ui-patterns/SKILL.md +104 -0
  403. package/pipeline/skills/figma-common/figma-utility/SKILL.md +274 -0
  404. package/pipeline/skills/figma-common/figma-utility/scripts/figma-utility.py +808 -0
  405. package/pipeline/skills/figma-common/figma-validate/SKILL.md +633 -0
  406. package/pipeline/skills/figma-common/performance-iteration-commit-all/SKILL.md +711 -0
  407. package/pipeline/skills/figma-common/performance-review-next/SKILL.md +233 -0
  408. package/pipeline/skills/figma-common/performance-start/SKILL.md +425 -0
  409. package/pipeline/skills/figma-common/performance-swiftui/SKILL.md +706 -0
  410. package/pipeline/skills/figma-common/performance-tour/SKILL.md +418 -0
  411. package/pipeline/skills/figma-ios/REVIEW_CHECKLIST.md +67 -0
  412. package/pipeline/skills/figma-ios/figma-component-code-connect/SKILL.md +178 -0
  413. package/pipeline/skills/figma-ios/figma-component-implement/SKILL.md +184 -0
  414. package/pipeline/skills/figma-ios/figma-component-test/SKILL.md +219 -0
  415. package/pipeline/skills/figma-ios/figma-component-wiki/SKILL.md +274 -0
  416. package/pipeline/skills/figma-ios/figma-to-component/SKILL.md +401 -0
  417. package/pipeline/skills/figma-ios/figma-to-component/halt-return-protocol.md +57 -0
  418. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-0-init.md +307 -0
  419. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-1-gathering.md +119 -0
  420. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-1.5-existing-discovery.md +174 -0
  421. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2-orchestrator.md +333 -0
  422. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2a-testing-identifiers.md +368 -0
  423. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2b-localization.md +393 -0
  424. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2c-accessibility.md +617 -0
  425. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2d-analytics.md +352 -0
  426. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3-orchestrator.md +337 -0
  427. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3a-location.md +206 -0
  428. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3b-tokens.md +235 -0
  429. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3c-nested.md +214 -0
  430. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3d-patterns.md +871 -0
  431. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3e-assets.md +156 -0
  432. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3f-utilities.md +175 -0
  433. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3g-property-coverage.md +176 -0
  434. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3h-variant-config.md +333 -0
  435. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4-orchestrator.md +412 -0
  436. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4a-configuration.md +336 -0
  437. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4b-view.md +695 -0
  438. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4c-documentation.md +332 -0
  439. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4d-preview.md +380 -0
  440. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4e-modifiers.md +262 -0
  441. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5-orchestrator.md +482 -0
  442. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5a-viewinspector.md +274 -0
  443. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5b-snapshot.md +636 -0
  444. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5c-unit.md +142 -0
  445. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-6-code-connect.md +547 -0
  446. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7-wiki.md +39 -0
  447. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7a-confluence-generate.md +659 -0
  448. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7a-wiki-generate.md +580 -0
  449. package/pipeline/skills/figma-ios/figma-to-component/phases/phase-8-cleanup.md +51 -0
  450. package/pipeline/skills/figma-ios/figma-to-component/reference/accessibility.md +129 -0
  451. package/pipeline/skills/figma-ios/figma-to-component/reference/analytics-events.md +64 -0
  452. package/pipeline/skills/figma-ios/figma-to-component/reference/code-connect.md +531 -0
  453. package/pipeline/skills/figma-ios/figma-to-component/reference/confluence-api.md +89 -0
  454. package/pipeline/skills/figma-ios/figma-to-component/reference/confluence-xhtml.md +155 -0
  455. package/pipeline/skills/figma-ios/figma-to-component/reference/figma-to-swiftui-effects.md +196 -0
  456. package/pipeline/skills/figma-ios/figma-to-component/reference/halt-return-protocol.md +57 -0
  457. package/pipeline/skills/figma-ios/figma-to-component/reference/localization-naming.md +89 -0
  458. package/pipeline/skills/figma-ios/figma-to-component/reference/macros.md +227 -0
  459. package/pipeline/skills/figma-ios/figma-to-component/reference/missing-tokens.md +157 -0
  460. package/pipeline/skills/figma-ios/figma-to-component/reference/orchestrator-discipline.md +90 -0
  461. package/pipeline/skills/figma-ios/figma-to-component/reference/registry.md +116 -0
  462. package/pipeline/skills/figma-ios/figma-to-component/reference/remote-mcp-script.md +153 -0
  463. package/pipeline/skills/figma-ios/figma-to-component/reference/rest-api-script.md +130 -0
  464. package/pipeline/skills/figma-ios/figma-to-component/reference/scripts-inventory.md +218 -0
  465. package/pipeline/skills/figma-ios/figma-to-component/reference/snapshot-testing.md +188 -0
  466. package/pipeline/skills/figma-ios/figma-to-component/reference/subcomponent-graph.md +93 -0
  467. package/pipeline/skills/figma-ios/figma-to-component/reference/testing-identifiers-naming.md +98 -0
  468. package/pipeline/skills/figma-ios/figma-to-component/reference/tools.md +261 -0
  469. package/pipeline/skills/figma-ios/figma-to-component/reference/viewinspector.md +147 -0
  470. package/pipeline/skills/figma-ios/figma-to-component/reference/wiki-to-confluence-mapping.md +182 -0
  471. package/pipeline/skills/figma-ios/figma-to-component/scripts/apply-author-login-map.py +185 -0
  472. package/pipeline/skills/figma-ios/figma-to-component/scripts/backfill-status.py +609 -0
  473. package/pipeline/skills/figma-ios/figma-to-component/scripts/build-author-registry.py +332 -0
  474. package/pipeline/skills/figma-ios/figma-to-component/scripts/bulk-sync-issues.py +261 -0
  475. package/pipeline/skills/figma-ios/figma-to-component/scripts/code-connect-data-gather.py +184 -0
  476. package/pipeline/skills/figma-ios/figma-to-component/scripts/code-connect-publish.sh +188 -0
  477. package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-component-status-upload.py +768 -0
  478. package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-component-status.py +191 -0
  479. package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-data-gather.py +420 -0
  480. package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-page-ids.json +94 -0
  481. package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-publish.py +336 -0
  482. package/pipeline/skills/figma-ios/figma-to-component/scripts/figma-subcomponent-graph.py +391 -0
  483. package/pipeline/skills/figma-ios/figma-to-component/scripts/figma-update.py +292 -0
  484. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/__init__.py +1 -0
  485. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/issue_sync_propagate.py +93 -0
  486. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/registry_writer.py +299 -0
  487. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_backfill_status.py +343 -0
  488. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_figma_update.py +206 -0
  489. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_figma_update_http.py +149 -0
  490. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_phase_clis.py +281 -0
  491. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_registry_writer.py +332 -0
  492. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_skill_figma_issue.py +176 -0
  493. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_skill_figma_review.py +98 -0
  494. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_update_issue.py +298 -0
  495. package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_update_issue_gh.py +195 -0
  496. package/pipeline/skills/figma-ios/figma-to-component/scripts/phase1-gather.py +1298 -0
  497. package/pipeline/skills/figma-ios/figma-to-component/scripts/phase2-finalize.py +228 -0
  498. package/pipeline/skills/figma-ios/figma-to-component/scripts/phase3-scripts.py +1089 -0
  499. package/pipeline/skills/figma-ios/figma-to-component/scripts/phase4-finalize.py +141 -0
  500. package/pipeline/skills/figma-ios/figma-to-component/scripts/phase5-finalize.py +106 -0
  501. package/pipeline/skills/figma-ios/figma-to-component/scripts/phase6-finalize.py +162 -0
  502. package/pipeline/skills/figma-ios/figma-to-component/scripts/phase7-finalize.py +105 -0
  503. package/pipeline/skills/figma-ios/figma-to-component/scripts/register-icons-codeconnect.py +179 -0
  504. package/pipeline/skills/figma-ios/figma-to-component/scripts/remote-mcp-fetch.py +260 -0
  505. package/pipeline/skills/figma-ios/figma-to-component/scripts/resolve-author-logins.py +260 -0
  506. package/pipeline/skills/figma-ios/figma-to-component/scripts/run-uicomponents-tests.sh +86 -0
  507. package/pipeline/skills/figma-ios/figma-to-component/scripts/sidebar-generator.py +321 -0
  508. package/pipeline/skills/figma-ios/figma-to-component/scripts/update-issue-from-registry.py +1470 -0
  509. package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase4.sh +176 -0
  510. package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase6.sh +147 -0
  511. package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase7a.py +629 -0
  512. package/pipeline/skills/shared/README.md +212 -0
  513. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +315 -0
  514. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +348 -0
  515. package/pipeline/skills/shared/core/multi-agent/SKILL.md +944 -0
  516. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +51 -0
  517. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +300 -0
  518. package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +63 -0
  519. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +64 -0
  520. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +56 -0
  521. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +36 -0
  522. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +42 -0
  523. package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +66 -0
  524. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +292 -0
  525. package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +35 -0
  526. package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +38 -0
  527. package/pipeline/skills/shared/core/multi-agent-kill/SKILL.md +41 -0
  528. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +87 -0
  529. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +37 -0
  530. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +53 -0
  531. package/pipeline/skills/shared/core/multi-agent-log/SKILL.md +28 -0
  532. package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +47 -0
  533. package/pipeline/skills/shared/core/multi-agent-purge/SKILL.md +42 -0
  534. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +191 -0
  535. package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +31 -0
  536. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +61 -0
  537. package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +61 -0
  538. package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +62 -0
  539. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +309 -0
  540. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +55 -0
  541. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +41 -0
  542. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +184 -0
  543. package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +44 -0
  544. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +34 -0
  545. package/pipeline/skills/shared/external/accessibility-compliance-accessibility-audit/SKILL.md +45 -0
  546. package/pipeline/skills/shared/external/agentflow/SKILL.md +199 -0
  547. package/pipeline/skills/shared/external/alarmkit/SKILL.md +438 -0
  548. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +584 -0
  549. package/pipeline/skills/shared/external/android-architecture/SKILL.md +407 -0
  550. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +153 -0
  551. package/pipeline/skills/shared/external/android-performance/SKILL.md +736 -0
  552. package/pipeline/skills/shared/external/android-security/SKILL.md +577 -0
  553. package/pipeline/skills/shared/external/android_ui_verification/SKILL.md +66 -0
  554. package/pipeline/skills/shared/external/api-patterns/SKILL.md +85 -0
  555. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +910 -0
  556. package/pipeline/skills/shared/external/app-clips/SKILL.md +436 -0
  557. package/pipeline/skills/shared/external/app-intents/SKILL.md +489 -0
  558. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +1076 -0
  559. package/pipeline/skills/shared/external/app-store-changelog/SKILL.md +75 -0
  560. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +409 -0
  561. package/pipeline/skills/shared/external/app-store-review/SKILL.md +411 -0
  562. package/pipeline/skills/shared/external/app-store-review/references/code-signing.md +259 -0
  563. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +90 -0
  564. package/pipeline/skills/shared/external/app-store-review/references/rejection-patterns.md +152 -0
  565. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +118 -0
  566. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +500 -0
  567. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +425 -0
  568. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +344 -0
  569. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +508 -0
  570. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +285 -0
  571. package/pipeline/skills/shared/external/architecture/SKILL.md +60 -0
  572. package/pipeline/skills/shared/external/authentication/SKILL.md +496 -0
  573. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +211 -0
  574. package/pipeline/skills/shared/external/background-processing/SKILL.md +499 -0
  575. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +390 -0
  576. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +461 -0
  577. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +425 -0
  578. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +462 -0
  579. package/pipeline/skills/shared/external/clean-code/SKILL.md +94 -0
  580. package/pipeline/skills/shared/external/closed-loop-delivery/SKILL.md +116 -0
  581. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +492 -0
  582. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +461 -0
  583. package/pipeline/skills/shared/external/compose-components/SKILL.md +441 -0
  584. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +436 -0
  585. package/pipeline/skills/shared/external/compose-testing/SKILL.md +527 -0
  586. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +425 -0
  587. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +409 -0
  588. package/pipeline/skills/shared/external/context-compression/SKILL.md +266 -0
  589. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +491 -0
  590. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +435 -0
  591. package/pipeline/skills/shared/external/core-motion/SKILL.md +388 -0
  592. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +405 -0
  593. package/pipeline/skills/shared/external/core-nfc/SKILL.md +495 -0
  594. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +420 -0
  595. package/pipeline/skills/shared/external/coreml/SKILL.md +458 -0
  596. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +765 -0
  597. package/pipeline/skills/shared/external/css-modern/SKILL.md +467 -0
  598. package/pipeline/skills/shared/external/database-patterns/SKILL.md +335 -0
  599. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +422 -0
  600. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +387 -0
  601. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +298 -0
  602. package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +37 -0
  603. package/pipeline/skills/shared/external/device-integrity/SKILL.md +477 -0
  604. package/pipeline/skills/shared/external/docker-expert/SKILL.md +413 -0
  605. package/pipeline/skills/shared/external/energykit/SKILL.md +460 -0
  606. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +541 -0
  607. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +483 -0
  608. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +326 -0
  609. package/pipeline/skills/shared/external/fastapi-pro/SKILL.md +190 -0
  610. package/pipeline/skills/shared/external/firebase/SKILL.md +61 -0
  611. package/pipeline/skills/shared/external/github-actions-templates/SKILL.md +348 -0
  612. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +552 -0
  613. package/pipeline/skills/shared/external/healthkit/SKILL.md +498 -0
  614. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +602 -0
  615. package/pipeline/skills/shared/external/help-skills/SKILL.md +166 -0
  616. package/pipeline/skills/shared/external/hig-components-content/SKILL.md +81 -0
  617. package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +95 -0
  618. package/pipeline/skills/shared/external/hig-components-status/SKILL.md +82 -0
  619. package/pipeline/skills/shared/external/hig-components-system/SKILL.md +101 -0
  620. package/pipeline/skills/shared/external/hig-foundations/SKILL.md +94 -0
  621. package/pipeline/skills/shared/external/hig-inputs/SKILL.md +110 -0
  622. package/pipeline/skills/shared/external/hig-patterns/SKILL.md +99 -0
  623. package/pipeline/skills/shared/external/hig-platforms/SKILL.md +81 -0
  624. package/pipeline/skills/shared/external/hig-technologies/SKILL.md +125 -0
  625. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +496 -0
  626. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +455 -0
  627. package/pipeline/skills/shared/external/html-semantic/SKILL.md +301 -0
  628. package/pipeline/skills/shared/external/humanizer/SKILL.md +118 -0
  629. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +301 -0
  630. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +140 -0
  631. package/pipeline/skills/shared/external/ios-debugger-agent/SKILL.md +59 -0
  632. package/pipeline/skills/shared/external/ios-developer/SKILL.md +217 -0
  633. package/pipeline/skills/shared/external/ios-localization/SKILL.md +418 -0
  634. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +627 -0
  635. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +462 -0
  636. package/pipeline/skills/shared/external/ios-networking/SKILL.md +441 -0
  637. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +862 -0
  638. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -0
  639. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +563 -0
  640. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +1116 -0
  641. package/pipeline/skills/shared/external/ios-security/SKILL.md +496 -0
  642. package/pipeline/skills/shared/external/ios-security/references/app-review-guidelines.md +174 -0
  643. package/pipeline/skills/shared/external/ios-security/references/cryptokit-advanced.md +297 -0
  644. package/pipeline/skills/shared/external/ios-security/references/file-storage-patterns.md +354 -0
  645. package/pipeline/skills/shared/external/ios-security/references/privacy-manifest.md +117 -0
  646. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +101 -0
  647. package/pipeline/skills/shared/external/live-activities/SKILL.md +500 -0
  648. package/pipeline/skills/shared/external/live-activities/references/live-activity-patterns.md +868 -0
  649. package/pipeline/skills/shared/external/macos-menubar-tuist-app/SKILL.md +109 -0
  650. package/pipeline/skills/shared/external/macos-spm-app-packaging/SKILL.md +110 -0
  651. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +485 -0
  652. package/pipeline/skills/shared/external/mapkit-location/references/corelocation-patterns.md +730 -0
  653. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +748 -0
  654. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +479 -0
  655. package/pipeline/skills/shared/external/monorepo-architect/SKILL.md +64 -0
  656. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +395 -0
  657. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +363 -0
  658. package/pipeline/skills/shared/external/natural-language/SKILL.md +412 -0
  659. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +311 -0
  660. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +418 -0
  661. package/pipeline/skills/shared/external/nodejs-backend-patterns/SKILL.md +38 -0
  662. package/pipeline/skills/shared/external/observability-engineer/SKILL.md +235 -0
  663. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +398 -0
  664. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +254 -0
  665. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +387 -0
  666. package/pipeline/skills/shared/external/pencilkit-drawing/references/paperkit-integration.md +376 -0
  667. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +302 -0
  668. package/pipeline/skills/shared/external/permissionkit/SKILL.md +446 -0
  669. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +435 -0
  670. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +501 -0
  671. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +701 -0
  672. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +774 -0
  673. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +869 -0
  674. package/pipeline/skills/shared/external/photos-camera-media/references/photospicker-patterns.md +597 -0
  675. package/pipeline/skills/shared/external/play-store-review/SKILL.md +350 -0
  676. package/pipeline/skills/shared/external/push-notifications/SKILL.md +501 -0
  677. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +677 -0
  678. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +745 -0
  679. package/pipeline/skills/shared/external/python-patterns/SKILL.md +383 -0
  680. package/pipeline/skills/shared/external/react-best-practices/SKILL.md +290 -0
  681. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +479 -0
  682. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +480 -0
  683. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +386 -0
  684. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +506 -0
  685. package/pipeline/skills/shared/external/room-database/SKILL.md +564 -0
  686. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +483 -0
  687. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +544 -0
  688. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +485 -0
  689. package/pipeline/skills/shared/external/storekit/SKILL.md +478 -0
  690. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +58 -0
  691. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +755 -0
  692. package/pipeline/skills/shared/external/swift-charts/SKILL.md +487 -0
  693. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +895 -0
  694. package/pipeline/skills/shared/external/swift-codable/SKILL.md +467 -0
  695. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +408 -0
  696. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +80 -0
  697. package/pipeline/skills/shared/external/swift-concurrency/references/swift-6-2-concurrency.md +233 -0
  698. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +187 -0
  699. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +341 -0
  700. package/pipeline/skills/shared/external/swift-concurrency-expert/SKILL.md +113 -0
  701. package/pipeline/skills/shared/external/swift-concurrency-pro/SKILL.md +124 -0
  702. package/pipeline/skills/shared/external/swift-concurrency-pro/references/actors.md +155 -0
  703. package/pipeline/skills/shared/external/swift-concurrency-pro/references/async-streams.md +67 -0
  704. package/pipeline/skills/shared/external/swift-concurrency-pro/references/bridging.md +52 -0
  705. package/pipeline/skills/shared/external/swift-concurrency-pro/references/bug-patterns.md +100 -0
  706. package/pipeline/skills/shared/external/swift-concurrency-pro/references/cancellation.md +107 -0
  707. package/pipeline/skills/shared/external/swift-concurrency-pro/references/diagnostics.md +70 -0
  708. package/pipeline/skills/shared/external/swift-concurrency-pro/references/hotspots.md +47 -0
  709. package/pipeline/skills/shared/external/swift-concurrency-pro/references/interop.md +129 -0
  710. package/pipeline/skills/shared/external/swift-concurrency-pro/references/new-features.md +224 -0
  711. package/pipeline/skills/shared/external/swift-concurrency-pro/references/structured.md +101 -0
  712. package/pipeline/skills/shared/external/swift-concurrency-pro/references/testing.md +218 -0
  713. package/pipeline/skills/shared/external/swift-concurrency-pro/references/unstructured.md +61 -0
  714. package/pipeline/skills/shared/external/swift-language/SKILL.md +498 -0
  715. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +505 -0
  716. package/pipeline/skills/shared/external/swift-testing/SKILL.md +462 -0
  717. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +504 -0
  718. package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +97 -0
  719. package/pipeline/skills/shared/external/swift-testing-pro/references/async-tests.md +252 -0
  720. package/pipeline/skills/shared/external/swift-testing-pro/references/core-rules.md +52 -0
  721. package/pipeline/skills/shared/external/swift-testing-pro/references/migrating-from-xctest.md +34 -0
  722. package/pipeline/skills/shared/external/swift-testing-pro/references/new-features.md +318 -0
  723. package/pipeline/skills/shared/external/swift-testing-pro/references/writing-better-tests.md +254 -0
  724. package/pipeline/skills/shared/external/swiftdata/SKILL.md +334 -0
  725. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +504 -0
  726. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +975 -0
  727. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +675 -0
  728. package/pipeline/skills/shared/external/swiftdata-pro/SKILL.md +102 -0
  729. package/pipeline/skills/shared/external/swiftdata-pro/references/class-inheritance.md +104 -0
  730. package/pipeline/skills/shared/external/swiftdata-pro/references/cloudkit.md +10 -0
  731. package/pipeline/skills/shared/external/swiftdata-pro/references/core-rules.md +20 -0
  732. package/pipeline/skills/shared/external/swiftdata-pro/references/indexing.md +27 -0
  733. package/pipeline/skills/shared/external/swiftdata-pro/references/predicates.md +73 -0
  734. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +503 -0
  735. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +821 -0
  736. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +553 -0
  737. package/pipeline/skills/shared/external/swiftui-expert-skill/SKILL.md +102 -0
  738. package/pipeline/skills/shared/external/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
  739. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md +403 -0
  740. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-basics.md +284 -0
  741. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-transitions.md +326 -0
  742. package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  743. package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts.md +602 -0
  744. package/pipeline/skills/shared/external/swiftui-expert-skill/references/image-optimization.md +203 -0
  745. package/pipeline/skills/shared/external/swiftui-expert-skill/references/latest-apis.md +464 -0
  746. package/pipeline/skills/shared/external/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  747. package/pipeline/skills/shared/external/swiftui-expert-skill/references/liquid-glass.md +416 -0
  748. package/pipeline/skills/shared/external/swiftui-expert-skill/references/list-patterns.md +394 -0
  749. package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-scenes.md +318 -0
  750. package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-views.md +357 -0
  751. package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  752. package/pipeline/skills/shared/external/swiftui-expert-skill/references/performance-patterns.md +403 -0
  753. package/pipeline/skills/shared/external/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  754. package/pipeline/skills/shared/external/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  755. package/pipeline/skills/shared/external/swiftui-expert-skill/references/state-management.md +417 -0
  756. package/pipeline/skills/shared/external/swiftui-expert-skill/references/view-structure.md +389 -0
  757. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +450 -0
  758. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +425 -0
  759. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +336 -0
  760. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +97 -0
  761. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +69 -0
  762. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +99 -0
  763. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +147 -0
  764. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +98 -0
  765. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +262 -0
  766. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +207 -0
  767. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +177 -0
  768. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +169 -0
  769. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +178 -0
  770. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +371 -0
  771. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +486 -0
  772. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +1097 -0
  773. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +780 -0
  774. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +696 -0
  775. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +487 -0
  776. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +46 -0
  777. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +29 -0
  778. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +33 -0
  779. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +52 -0
  780. package/pipeline/skills/shared/external/swiftui-performance-audit/SKILL.md +114 -0
  781. package/pipeline/skills/shared/external/swiftui-pro/SKILL.md +108 -0
  782. package/pipeline/skills/shared/external/swiftui-pro/references/accessibility.md +13 -0
  783. package/pipeline/skills/shared/external/swiftui-pro/references/api.md +39 -0
  784. package/pipeline/skills/shared/external/swiftui-pro/references/data.md +43 -0
  785. package/pipeline/skills/shared/external/swiftui-pro/references/design.md +31 -0
  786. package/pipeline/skills/shared/external/swiftui-pro/references/hygiene.md +9 -0
  787. package/pipeline/skills/shared/external/swiftui-pro/references/navigation.md +14 -0
  788. package/pipeline/skills/shared/external/swiftui-pro/references/performance.md +46 -0
  789. package/pipeline/skills/shared/external/swiftui-pro/references/swift.md +56 -0
  790. package/pipeline/skills/shared/external/swiftui-pro/references/views.md +35 -0
  791. package/pipeline/skills/shared/external/swiftui-ui-patterns/SKILL.md +103 -0
  792. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +428 -0
  793. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +534 -0
  794. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +948 -0
  795. package/pipeline/skills/shared/external/swiftui-view-refactor/SKILL.md +210 -0
  796. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +273 -0
  797. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +151 -0
  798. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +95 -0
  799. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +51 -0
  800. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +111 -0
  801. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +309 -0
  802. package/pipeline/skills/shared/external/testing-backend/SKILL.md +393 -0
  803. package/pipeline/skills/shared/external/tipkit/SKILL.md +494 -0
  804. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +782 -0
  805. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +336 -0
  806. package/pipeline/skills/shared/external/vision-framework/SKILL.md +475 -0
  807. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +736 -0
  808. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +738 -0
  809. package/pipeline/skills/shared/external/vue-composition/SKILL.md +371 -0
  810. package/pipeline/skills/shared/external/weatherkit/SKILL.md +410 -0
  811. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +567 -0
  812. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +373 -0
  813. package/pipeline/skills/shared/external/web-performance/SKILL.md +345 -0
  814. package/pipeline/skills/shared/external/web-testing/SKILL.md +385 -0
  815. package/pipeline/skills/shared/external/widgetkit/SKILL.md +497 -0
  816. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +871 -0
  817. package/pipeline/skills/skills-index.md +205 -0
@@ -0,0 +1,1470 @@
1
+ #!/usr/bin/env python3
2
+ """update-issue-from-registry.py — full GitHub sync driven by registry.
3
+
4
+ Per-flag state is stored as 4 single-select Projects V2 fields on the
5
+ "Figma Components" board:
6
+
7
+ Implementation Gray / Green / Yellow
8
+ Tested Gray / Green / Yellow
9
+ Code Connect Gray / Green / Yellow
10
+ Wiki Gray / Green / Yellow
11
+
12
+ These fields replace the label-based approach — GitHub labels can't
13
+ vary color per-issue, but project fields can.
14
+
15
+ For one node-id, this script:
16
+
17
+ 1. Loads the registry entry.
18
+ 2. Skips if `skip: true` or the entry is an icon (`componentName`
19
+ starts with `Image.` on iOS or `Images.` on android).
20
+ 3. Computes the 4 flag states (`gray`/`green`/`yellow`) from the
21
+ registry's `status.<platform>` block.
22
+ 4. Finds all open issues whose body references the node-id.
23
+ 5. Zero matches → creates a new issue (team:core default, labels:
24
+ component/redesign/team:core), adds it to the project, sets the
25
+ 4 flag fields from registry state.
26
+ 6. Multiple matches → applies the duplicate rule (issue with
27
+ sub-issues wins; tiebreak by lowest number), closes losers with a
28
+ "Duplicate of #N" comment.
29
+ 7. On the survivor: rewrites the `### Progress` table in the body +
30
+ sets the 4 project field values.
31
+ 8. Idempotent — re-running after a successful sync produces no writes.
32
+
33
+ No label manipulation beyond what `gh issue create` applies at birth.
34
+ The 8 flag labels (`implemented:green` etc.) from the previous
35
+ iteration have been deleted from the repo; nothing writes `phase:*`
36
+ labels anymore.
37
+
38
+ Usage:
39
+ python3 update-issue-from-registry.py \\
40
+ --node-id 5:17752 \\
41
+ [--platform ios] \\
42
+ [--registry-dir /path] \\
43
+ [--repo OWNER/REPO] \\
44
+ [--team-label team:core] \\
45
+ [--dry-run]
46
+
47
+ Exit codes:
48
+ 0 — success
49
+ 1 — error
50
+ """
51
+
52
+ import argparse
53
+ import json
54
+ import re
55
+ import subprocess
56
+ import sys
57
+ from datetime import date
58
+ from pathlib import Path
59
+ from typing import List, Optional, Tuple
60
+
61
+ DEFAULT_REPO = "{github.componentsRepo}"
62
+ DEFAULT_TEAM_LABEL = "team:core"
63
+
64
+ # Projects V2 — "Figma Components" board
65
+ PROJECT_ID = "{github.projectV2Id}"
66
+
67
+ # Repo-level labels for flag state. ONE per flag (4 total). Label
68
+ # present = flag is green (done and current). Label absent = flag is
69
+ # gray (not done) or yellow (stale). Yellow state is tracked on the
70
+ # Projects V2 fields only — the label only fires on green, so the
71
+ # issue list stays uncluttered and shows "what's done."
72
+ FLAG_LABEL_NAMES = {
73
+ "implemented": "implemented",
74
+ "tested": "tested",
75
+ "codeConnect": "code-connected",
76
+ "wiki": "wiki",
77
+ }
78
+
79
+ # All 4 flag labels (as a tuple for set operations)
80
+ ALL_FLAG_LABELS = tuple(FLAG_LABEL_NAMES.values())
81
+
82
+ # Legacy labels that we strip whenever we see them. Includes the old
83
+ # phase:* enum and the 8 green/yellow flag labels from earlier
84
+ # iterations of this script.
85
+ LEGACY_PHASE_LABELS = (
86
+ "phase:implementation",
87
+ "phase:testing",
88
+ "phase:code-connect",
89
+ "phase:documentation",
90
+ "phase:done",
91
+ "impl:green", "impl:yellow",
92
+ "test:green", "test:yellow",
93
+ "cc:green", "cc:yellow",
94
+ "implemented:green", "implemented:yellow",
95
+ "tested:green", "tested:yellow",
96
+ "code-connected:green", "code-connected:yellow",
97
+ "wiki:green", "wiki:yellow",
98
+ )
99
+
100
+ # Field + option IDs captured at field-creation time (2026-04-11).
101
+ # Each flag has 3 options: gray (not done), green (current), yellow (stale).
102
+ FLAG_FIELDS: dict = {
103
+ "implemented": {
104
+ "field_id": "PVTSSF_lADOD594Cs4BSatBzhBdns8",
105
+ "options": {
106
+ "gray": "7936be8c",
107
+ "green": "8b8e2dac",
108
+ "yellow": "f11b325f",
109
+ },
110
+ },
111
+ "tested": {
112
+ "field_id": "PVTSSF_lADOD594Cs4BSatBzhBdntA",
113
+ "options": {
114
+ "gray": "2c4acdbe",
115
+ "green": "119380d9",
116
+ "yellow": "fabaa558",
117
+ },
118
+ },
119
+ "codeConnect": {
120
+ "field_id": "PVTSSF_lADOD594Cs4BSatBzhBdnt4",
121
+ "options": {
122
+ "gray": "62cb9ea4",
123
+ "green": "f226993d",
124
+ "yellow": "0317c874",
125
+ },
126
+ },
127
+ "wiki": {
128
+ "field_id": "PVTSSF_lADOD594Cs4BSatBzhBdnt8",
129
+ "options": {
130
+ "gray": "6b3f27bb",
131
+ "green": "5facabd2",
132
+ "yellow": "e6d08d85",
133
+ },
134
+ },
135
+ }
136
+
137
+ # Status column — single-select field that drives the kanban board view.
138
+ # Option ids captured at 2026-04-12 field expansion.
139
+ STATUS_FIELD_ID = "{github.projectV2Fields.status}"
140
+ STATUS_OPTIONS: dict = {
141
+ "Todo": "25f3e7a3",
142
+ "In Develop": "bc6c45f7",
143
+ "In Review": "bdb0d8ef",
144
+ "Done": "6275b69e",
145
+ "Blocked": "95949fd8",
146
+ "Bugfix": "e46fa72d",
147
+ "Stale": "457a1e74",
148
+ }
149
+
150
+ # Review Progress field — single-select replaces the review:0/4..review:4/4 labels.
151
+ REVIEW_PROGRESS_FIELD_ID = "PVTSSF_lADOD594Cs4BSatBzhBgYNs"
152
+ REVIEW_PROGRESS_OPTIONS: dict = {
153
+ "0/4": "d33443cf",
154
+ "1/4": "5f0bba11",
155
+ "2/4": "45016b2b",
156
+ "3/4": "2c52a38a",
157
+ "4/4": "04e0358a",
158
+ }
159
+
160
+ # Bugfix signal — if any of these labels are present on the issue, the column is Bugfix.
161
+ BUGFIX_LABELS = {"review-fix", "bug", "bugfix"}
162
+
163
+ # Review label legacy fallback: review:0/4 .. review:4/4
164
+ REVIEW_LABEL_RE = re.compile(r"^review:(\d)/4$")
165
+
166
+ # Progress-table section matcher.
167
+ PROGRESS_TABLE_RE = re.compile(
168
+ r"(### Progress\n\n)(.*?)(?=\n### |\Z)",
169
+ re.DOTALL,
170
+ )
171
+
172
+
173
+ # --------------------------------------------------------------------
174
+ # Repo layout
175
+ # --------------------------------------------------------------------
176
+
177
+ def find_repo_root() -> Path:
178
+ p = Path.cwd()
179
+ while p != p.parent:
180
+ if (p / ".git").exists():
181
+ return p
182
+ p = p.parent
183
+ return Path.cwd()
184
+
185
+
186
+ def default_registry_dir() -> Path:
187
+ return (
188
+ find_repo_root()
189
+ / "{repos.packagesContainer}"
190
+ / "Packages"
191
+ / "{project.slug}-common"
192
+ / "Shared"
193
+ / "Figma"
194
+ / "Components"
195
+ )
196
+
197
+
198
+ # --------------------------------------------------------------------
199
+ # Registry loading
200
+ # --------------------------------------------------------------------
201
+
202
+ def load_entry(registry_dir: Path, node_id: str) -> Optional[dict]:
203
+ path = Path(registry_dir) / f"{node_id.replace(':', '-')}.json"
204
+ if not path.exists():
205
+ return None
206
+ try:
207
+ return json.loads(path.read_text(encoding="utf-8"))
208
+ except (json.JSONDecodeError, OSError):
209
+ return None
210
+
211
+
212
+ def is_icon_entry(entry: dict) -> bool:
213
+ """Icons have componentName starting with 'Image.' on iOS or 'Images.'
214
+ on android. Generated in bulk — never get individual issues."""
215
+ cn = entry.get("componentName")
216
+ if isinstance(cn, dict):
217
+ ios = cn.get("ios") or ""
218
+ android = cn.get("android") or ""
219
+ elif isinstance(cn, str):
220
+ ios = cn
221
+ android = ""
222
+ else:
223
+ return False
224
+ if isinstance(ios, str) and ios.startswith("Image."):
225
+ return True
226
+ if isinstance(android, str) and android.startswith("Images."):
227
+ return True
228
+ return False
229
+
230
+
231
+ # --------------------------------------------------------------------
232
+ # Pure logic — flag state computation
233
+ # --------------------------------------------------------------------
234
+
235
+ def get_implemented_by(entry: dict, platform: str) -> Optional[str]:
236
+ """Return status.<platform>.implementedBy if set, else None.
237
+
238
+ Stored as a plain string (git author name). May differ from a
239
+ real GitHub login — the backfill stores whatever git log returned.
240
+ For this repo, git author names match GitHub logins (e.g.
241
+ M-ISPIRLI_tkgithub), so we pass them directly to gh as assignees.
242
+ """
243
+ ps = (entry.get("status") or {}).get(platform)
244
+ if not isinstance(ps, dict):
245
+ return None
246
+ author = ps.get("implementedBy")
247
+ return author if isinstance(author, str) and author else None
248
+
249
+
250
+ def compute_flag_states(entry: dict, platform: str) -> dict:
251
+ """Return {flag_name: 'gray'|'green'|'yellow'} for all 4 flags.
252
+
253
+ Missing flags default to 'gray'. Never omits a key.
254
+
255
+ Rules:
256
+ implemented:
257
+ value==false → gray
258
+ value==true, stale==false → green
259
+ value==true, stale==true → yellow
260
+
261
+ tested / codeConnect / wiki:
262
+ value==false → gray
263
+ value==true, against >= impl.version → green
264
+ value==true, against < impl.version → yellow
265
+ """
266
+ result: dict = {
267
+ "implemented": "gray",
268
+ "tested": "gray",
269
+ "codeConnect": "gray",
270
+ "wiki": "gray",
271
+ }
272
+
273
+ ps = (entry.get("status") or {}).get(platform)
274
+ if not isinstance(ps, dict):
275
+ return result
276
+
277
+ impl = ps.get("implemented") or {}
278
+ if impl.get("value"):
279
+ result["implemented"] = "yellow" if ps.get("stale") else "green"
280
+
281
+ impl_version = impl.get("version", 0)
282
+
283
+ for flag in ("tested", "codeConnect", "wiki"):
284
+ f = ps.get(flag) or {}
285
+ if not f.get("value"):
286
+ continue
287
+ if f.get("against", 0) < impl_version:
288
+ result[flag] = "yellow"
289
+ else:
290
+ result[flag] = "green"
291
+
292
+ return result
293
+
294
+
295
+ # --------------------------------------------------------------------
296
+ # Sub-component graph (for Blocked column) — lazy loaded + cached
297
+ # --------------------------------------------------------------------
298
+
299
+ _sub_graph_cache: Optional[dict] = None
300
+ _name_to_node_cache: Optional[dict] = None
301
+
302
+
303
+ def load_sub_component_graph(common_path: Optional[Path] = None) -> dict:
304
+ """Return {nodeId: [subComponentName, ...]} keyed by colon-form node-id.
305
+
306
+ Cached on first call. Returns {} if the graph file is missing."""
307
+ global _sub_graph_cache
308
+ if _sub_graph_cache is not None:
309
+ return _sub_graph_cache
310
+ if common_path is None:
311
+ common_path = find_repo_root() / "{repos.packagesContainer}" / "Packages" / "{project.slug}-common"
312
+ graph_path = common_path / "Shared" / "Figma" / "sub-component-graph.json"
313
+ try:
314
+ with graph_path.open() as f:
315
+ raw = json.load(f)
316
+ g = raw.get("graph", {}) or {}
317
+ _sub_graph_cache = {
318
+ nid: (entry.get("subComponents") or [])
319
+ for nid, entry in g.items()
320
+ if isinstance(entry, dict)
321
+ }
322
+ except (OSError, json.JSONDecodeError):
323
+ _sub_graph_cache = {}
324
+ return _sub_graph_cache
325
+
326
+
327
+ def build_name_to_node_index(registry_dir: Path, platform: str) -> dict:
328
+ """Return {componentName: nodeId-dash-form} by walking the registry.
329
+
330
+ Cached on first call. Used to resolve sub-component names (from the
331
+ graph) back to registry entries so we can check their implemented
332
+ state for the Blocked column."""
333
+ global _name_to_node_cache
334
+ if _name_to_node_cache is not None:
335
+ return _name_to_node_cache
336
+ index: dict = {}
337
+ try:
338
+ for p in registry_dir.glob("*.json"):
339
+ try:
340
+ with p.open() as f:
341
+ d = json.load(f)
342
+ except (OSError, json.JSONDecodeError):
343
+ continue
344
+ if not isinstance(d, dict):
345
+ continue
346
+ cn_obj = d.get("componentName") or {}
347
+ cn = cn_obj.get(platform) if isinstance(cn_obj, dict) else None
348
+ if cn:
349
+ index[cn] = p.stem # dash-form node id
350
+ except OSError:
351
+ pass
352
+ _name_to_node_cache = index
353
+ return _name_to_node_cache
354
+
355
+
356
+ def is_blocked(
357
+ node_id: str,
358
+ registry_dir: Path,
359
+ platform: str,
360
+ common_path: Optional[Path] = None,
361
+ ) -> bool:
362
+ """Return True if any sub-component of the given node is NOT implemented.
363
+
364
+ Looks up the node in sub-component-graph.json (colon-form keys),
365
+ then for each sub-component name resolves it back to its own registry
366
+ entry via a name→node index, and checks `status.<platform>.implemented.value`.
367
+
368
+ Skipped, icon, and registry-missing children are treated as 'not blocking'.
369
+ """
370
+ graph = load_sub_component_graph(common_path)
371
+ key_colon = node_id.replace("-", ":")
372
+ subs = graph.get(key_colon) or []
373
+ if not subs:
374
+ return False
375
+ name_index = build_name_to_node_index(registry_dir, platform)
376
+ for sub_name in subs:
377
+ sub_nid_dash = name_index.get(sub_name)
378
+ if not sub_nid_dash:
379
+ continue # not in registry — can't assess, assume OK
380
+ sub_entry = load_entry(registry_dir, sub_nid_dash)
381
+ if not isinstance(sub_entry, dict):
382
+ continue
383
+ if sub_entry.get("skip") or is_icon_entry(sub_entry):
384
+ continue
385
+ sub_status = sub_entry.get("status")
386
+ if not isinstance(sub_status, dict):
387
+ continue
388
+ sub_platform = sub_status.get(platform)
389
+ if not isinstance(sub_platform, dict):
390
+ continue
391
+ sub_impl = sub_platform.get("implemented") or {}
392
+ if not isinstance(sub_impl, dict):
393
+ continue
394
+ if not sub_impl.get("value"):
395
+ return True
396
+ return False
397
+
398
+
399
+ # --------------------------------------------------------------------
400
+ # Review progress + Status column (Projects V2 kanban view)
401
+ # --------------------------------------------------------------------
402
+
403
+ def parse_review_progress_from_labels(label_names: List[str]) -> int:
404
+ """Read review:N/4 labels, return N (0..4). Defaults to 0 if absent.
405
+
406
+ Used as a fallback until the Review Progress single-select field is
407
+ authoritative everywhere."""
408
+ best = 0
409
+ for name in label_names:
410
+ m = REVIEW_LABEL_RE.match(name)
411
+ if m:
412
+ n = int(m.group(1))
413
+ if n > best:
414
+ best = n
415
+ return best
416
+
417
+
418
+ def has_bugfix_signal(label_names: List[str], open_bug_sub_issues: int = 0) -> bool:
419
+ """Return True if the issue has any active bugfix signal.
420
+
421
+ Two independent signals (either one is enough):
422
+ 1. Parent issue itself carries a review-fix / bug / bugfix label.
423
+ 2. There are open sub-issues with those labels (counted externally).
424
+
425
+ Checking both makes the system self-healing: if someone forgets to
426
+ remove the label after all bugs are closed, the sub-issue count
427
+ catches it (and vice versa — if the label is missing but open bugs
428
+ exist, we still detect it).
429
+ """
430
+ if any(name in BUGFIX_LABELS for name in label_names):
431
+ return True
432
+ return open_bug_sub_issues > 0
433
+
434
+
435
+ def count_open_bug_sub_issues(owner: str, name: str, issue_number: int) -> int:
436
+ """Count open sub-issues that carry a bugfix label (review-fix, bug, bugfix).
437
+
438
+ Uses the existing list_sub_issues() helper to get sub-issue numbers,
439
+ then checks each for open state + bug label. Cached per-call — not
440
+ expensive for typical 0-3 bug sub-issues per component.
441
+ """
442
+ try:
443
+ sub_numbers = list_sub_issues(owner, name, issue_number)
444
+ except RuntimeError:
445
+ return 0
446
+ count = 0
447
+ for sn in sub_numbers:
448
+ try:
449
+ raw = gh(["issue", "view", str(sn), "--repo", f"{owner}/{name}",
450
+ "--json", "state,labels"])
451
+ data = json.loads(raw)
452
+ if data.get("state") != "OPEN":
453
+ continue
454
+ labels = {(l.get("name") or "") for l in (data.get("labels") or []) if isinstance(l, dict)}
455
+ if labels & BUGFIX_LABELS:
456
+ count += 1
457
+ except (RuntimeError, json.JSONDecodeError):
458
+ continue
459
+ return count
460
+
461
+
462
+ def effective_review_progress(label_names: List[str], flag_states: dict) -> int:
463
+ """Return the effective review count, accounting for staleness.
464
+
465
+ Reviews become stale (reset to 0) when tested or codeConnect are
466
+ YELLOW — meaning the implementation version bumped since those phases
467
+ last ran. This happens after a bugfix or re-implementation.
468
+
469
+ Reviews are NOT reset when tested/codeConnect are GRAY (never run).
470
+ Testing and wiki are nice-to-have, not prerequisites for reviews.
471
+ Only impl + codeConnect green are mandatory for In Review; tests
472
+ and wiki are tracked per-flag but don't gate the review cycle.
473
+ """
474
+ raw = parse_review_progress_from_labels(label_names)
475
+ tested = flag_states.get("tested", "gray")
476
+ cc = flag_states.get("codeConnect", "gray")
477
+ # Only yellow (stale) resets reviews — gray (never done) does not.
478
+ if tested == "yellow" or cc == "yellow":
479
+ return 0 # stale — impl version changed since phases ran
480
+ return raw
481
+
482
+
483
+ def compute_status_column(
484
+ flag_states: dict,
485
+ label_names: List[str],
486
+ has_assignees: bool,
487
+ blocked: bool,
488
+ open_bug_sub_issues: int = 0,
489
+ ) -> str:
490
+ """Return the target Status column name using the precedence chain:
491
+
492
+ Stale > Bugfix > Blocked > Done > In Review > In Develop > Todo
493
+
494
+ Inputs:
495
+ flag_states : {implemented,tested,codeConnect,wiki} -> gray|green|yellow
496
+ label_names : issue label names (bugfix signal + legacy review count)
497
+ has_assignees : True if issue has >=1 assignee
498
+ blocked : precomputed from is_blocked()
499
+ open_bug_sub_issues : count of open sub-issues with bugfix labels
500
+ """
501
+ impl = flag_states.get("implemented", "gray")
502
+ cc = flag_states.get("codeConnect", "gray")
503
+
504
+ # 1. Stale — only implementation yellow counts (downstream yellow is per-flag)
505
+ if impl == "yellow":
506
+ return "Stale"
507
+
508
+ # 2. Bugfix — review-fix/bug labels present OR open bug sub-issues
509
+ if has_bugfix_signal(label_names, open_bug_sub_issues):
510
+ return "Bugfix"
511
+
512
+ # 3. Blocked — a sub-component is not implemented
513
+ if blocked and impl != "green":
514
+ return "Blocked"
515
+
516
+ # 4. Done — effective 4/4 reviews passed (stale reviews = 0)
517
+ reviews = effective_review_progress(label_names, flag_states)
518
+ if reviews >= 4 and impl == "green":
519
+ return "Done"
520
+
521
+ # 5. In Review — implementation + code connect both green
522
+ if impl == "green" and cc == "green":
523
+ return "In Review"
524
+
525
+ # 6. In Develop — claimed (has assignee) but not implemented yet
526
+ if has_assignees and impl != "green":
527
+ return "In Develop"
528
+
529
+ # 7. Todo — default
530
+ return "Todo"
531
+
532
+
533
+ def review_progress_option_name(n: int) -> str:
534
+ """Clamp review count to a valid Review Progress option name."""
535
+ if n < 0:
536
+ n = 0
537
+ if n > 4:
538
+ n = 4
539
+ return f"{n}/4"
540
+
541
+
542
+ # --------------------------------------------------------------------
543
+ # Progress table (body representation)
544
+ # --------------------------------------------------------------------
545
+
546
+ def build_progress_table(flag_states: dict) -> str:
547
+ ICON = {"gray": "⚪", "green": "🟢", "yellow": "🟡"}
548
+ return (
549
+ "| Flag | State |\n"
550
+ "|---|---|\n"
551
+ f"| Implementation | {ICON[flag_states.get('implemented', 'gray')]} |\n"
552
+ f"| Testing | {ICON[flag_states.get('tested', 'gray')]} |\n"
553
+ f"| Code Connect | {ICON[flag_states.get('codeConnect', 'gray')]} |\n"
554
+ f"| Wiki | {ICON[flag_states.get('wiki', 'gray')]} |\n"
555
+ )
556
+
557
+
558
+ def rewrite_progress_table(body: str, flag_states: dict) -> str:
559
+ new_table = build_progress_table(flag_states)
560
+ new_block = new_table + "\n"
561
+
562
+ def _sub(m: "re.Match[str]") -> str:
563
+ return m.group(1) + new_block
564
+
565
+ new_body, n = PROGRESS_TABLE_RE.subn(_sub, body, count=1)
566
+ if n == 0:
567
+ return body.rstrip() + "\n\n### Progress\n\n" + new_block
568
+ return new_body
569
+
570
+
571
+ # --------------------------------------------------------------------
572
+ # gh wrapper — always captures stdout
573
+ # --------------------------------------------------------------------
574
+
575
+ GH_TIMEOUT_SECONDS = 30 # hard timeout per gh invocation — prevents infinite hangs
576
+
577
+
578
+ def gh(args: List[str], dry_run: bool = False, timeout: int = GH_TIMEOUT_SECONDS) -> str:
579
+ if dry_run:
580
+ print(f"[dry-run] gh {' '.join(args)}", file=sys.stderr)
581
+ return ""
582
+ try:
583
+ r = subprocess.run(
584
+ ["gh"] + args,
585
+ capture_output=True,
586
+ text=True,
587
+ check=False,
588
+ timeout=timeout,
589
+ )
590
+ except FileNotFoundError:
591
+ raise RuntimeError("gh CLI not found on PATH")
592
+ except subprocess.TimeoutExpired:
593
+ raise RuntimeError(f"gh {' '.join(args[:3])}... timed out after {timeout}s")
594
+ if r.returncode != 0:
595
+ raise RuntimeError(f"gh {' '.join(args)} failed: {r.stderr.strip()}")
596
+ return r.stdout.strip()
597
+
598
+
599
+ def gh_graphql(query: str, variables: Optional[dict] = None, dry_run: bool = False) -> dict:
600
+ """Run a GraphQL query/mutation via gh api graphql. Returns parsed JSON."""
601
+ if dry_run:
602
+ print(f"[dry-run] gh api graphql (query len={len(query)})", file=sys.stderr)
603
+ return {}
604
+ args = ["api", "graphql", "-f", f"query={query}"]
605
+ if variables:
606
+ for k, v in variables.items():
607
+ args += ["-F", f"{k}={v}"]
608
+ out = gh(args, dry_run=False)
609
+ try:
610
+ data = json.loads(out)
611
+ except json.JSONDecodeError:
612
+ raise RuntimeError(f"graphql: could not parse response: {out[:200]}")
613
+ if "errors" in data:
614
+ raise RuntimeError(f"graphql errors: {data['errors']}")
615
+ return data.get("data", {})
616
+
617
+
618
+ # --------------------------------------------------------------------
619
+ # GitHub operations — REST (issue list/comment/close)
620
+ # --------------------------------------------------------------------
621
+
622
+ def find_all_open_issues(node_id: str, repo: str, dry_run: bool) -> List[dict]:
623
+ """Return open issues referencing the node-id, with sub-issue counts.
624
+
625
+ Uses gh issue list for initial filtering, then GraphQL for
626
+ sub-issue counts per match. Sorted by number ascending.
627
+ """
628
+ out = gh(
629
+ [
630
+ "issue", "list",
631
+ "--repo", repo,
632
+ "--state", "open",
633
+ "--search", f'"{node_id}" in:body',
634
+ "--json", "number,title,body,labels",
635
+ "--limit", "20",
636
+ ],
637
+ dry_run=dry_run,
638
+ )
639
+ if dry_run or not out:
640
+ return []
641
+ try:
642
+ data = json.loads(out)
643
+ except json.JSONDecodeError:
644
+ return []
645
+ if not isinstance(data, list):
646
+ return []
647
+
648
+ # Annotate each with sub_issue_count via GraphQL
649
+ owner, name = repo.split("/", 1)
650
+ for issue in data:
651
+ num = issue.get("number")
652
+ if not num:
653
+ issue["sub_issue_count"] = 0
654
+ continue
655
+ try:
656
+ issue["sub_issue_count"] = count_sub_issues(owner, name, num)
657
+ except RuntimeError:
658
+ issue["sub_issue_count"] = 0
659
+
660
+ return sorted(data, key=lambda d: d.get("number", 0))
661
+
662
+
663
+ def count_sub_issues(owner: str, name: str, issue_number: int) -> int:
664
+ q = """
665
+ query($owner: String!, $name: String!, $number: Int!) {
666
+ repository(owner: $owner, name: $name) {
667
+ issue(number: $number) {
668
+ subIssues(first: 1) { totalCount }
669
+ }
670
+ }
671
+ }
672
+ """
673
+ data = gh_graphql(q, {"owner": owner, "name": name, "number": issue_number})
674
+ try:
675
+ return data["repository"]["issue"]["subIssues"]["totalCount"]
676
+ except (KeyError, TypeError):
677
+ return 0
678
+
679
+
680
+ def get_issue_assignees(issue_number: int, repo: str) -> List[str]:
681
+ """Return list of assignee logins on an issue, or []."""
682
+ out = gh([
683
+ "issue", "view", str(issue_number),
684
+ "--repo", repo,
685
+ "--json", "assignees",
686
+ ])
687
+ try:
688
+ data = json.loads(out)
689
+ except json.JSONDecodeError:
690
+ return []
691
+ return [a.get("login", "") for a in (data.get("assignees") or []) if isinstance(a, dict) and a.get("login")]
692
+
693
+
694
+ def add_assignees(issue_number: int, repo: str, logins: List[str], dry_run: bool) -> None:
695
+ """Add assignees to an issue via gh issue edit --add-assignee.
696
+
697
+ Non-fatal on errors — assignee names may not map to real GitHub
698
+ logins (backfill stores git author names, which usually match but
699
+ not always). Failure is logged, not raised.
700
+ """
701
+ if not logins:
702
+ return
703
+ try:
704
+ gh(
705
+ ["issue", "edit", str(issue_number), "--repo", repo,
706
+ "--add-assignee", ",".join(logins)],
707
+ dry_run=dry_run,
708
+ )
709
+ except RuntimeError:
710
+ pass
711
+
712
+
713
+ def list_sub_issues(owner: str, name: str, issue_number: int) -> List[int]:
714
+ """Return list of sub-issue numbers for one issue via GraphQL.
715
+
716
+ Used during duplicate conflation to move sub-issues from the
717
+ loser to the winner before closing the loser. Returns [] if the
718
+ query fails or the issue has no sub-issues.
719
+ """
720
+ q = """
721
+ query($owner: String!, $name: String!, $number: Int!) {
722
+ repository(owner: $owner, name: $name) {
723
+ issue(number: $number) {
724
+ subIssues(first: 50) {
725
+ nodes { number id }
726
+ }
727
+ }
728
+ }
729
+ }
730
+ """
731
+ try:
732
+ data = gh_graphql(q, {"owner": owner, "name": name, "number": issue_number})
733
+ except RuntimeError:
734
+ return []
735
+ try:
736
+ nodes = data["repository"]["issue"]["subIssues"]["nodes"]
737
+ except (KeyError, TypeError):
738
+ return []
739
+ return [n.get("number") for n in (nodes or []) if n and n.get("number")]
740
+
741
+
742
+ def reparent_sub_issue(
743
+ owner: str,
744
+ name: str,
745
+ sub_issue_number: int,
746
+ new_parent_number: int,
747
+ ) -> bool:
748
+ """Move a sub-issue from its current parent to a new parent.
749
+
750
+ Uses the addSubIssue mutation, which re-links if the sub-issue is
751
+ already tracked under another parent (GitHub's own behavior: a
752
+ sub-issue has exactly one parent at a time).
753
+ """
754
+ q_id = """
755
+ query($owner: String!, $name: String!, $sub: Int!, $parent: Int!) {
756
+ repository(owner: $owner, name: $name) {
757
+ sub: issue(number: $sub) { id }
758
+ parent: issue(number: $parent) { id }
759
+ }
760
+ }
761
+ """
762
+ try:
763
+ data = gh_graphql(q_id, {"owner": owner, "name": name, "sub": sub_issue_number, "parent": new_parent_number})
764
+ except RuntimeError:
765
+ return False
766
+ try:
767
+ sub_id = data["repository"]["sub"]["id"]
768
+ parent_id = data["repository"]["parent"]["id"]
769
+ except (KeyError, TypeError):
770
+ return False
771
+
772
+ mut = """
773
+ mutation($issueId: ID!, $subIssueId: ID!) {
774
+ addSubIssue(input: {issueId: $issueId, subIssueId: $subIssueId}) {
775
+ issue { number }
776
+ }
777
+ }
778
+ """
779
+ try:
780
+ gh_graphql(mut, {"issueId": parent_id, "subIssueId": sub_id})
781
+ return True
782
+ except RuntimeError:
783
+ return False
784
+
785
+
786
+ def conflate_duplicate(
787
+ loser: dict,
788
+ keeper_number: int,
789
+ repo: str,
790
+ dry_run: bool,
791
+ ) -> dict:
792
+ """Full duplicate conflation — transfer state from loser to keeper, then close loser.
793
+
794
+ Transfers:
795
+ 1. Assignees (add loser's assignees to keeper)
796
+ 2. Sub-issues (reparent each to keeper)
797
+ Then:
798
+ 3. Posts a Duplicate-of comment on the loser
799
+ 4. Closes the loser as 'not planned'
800
+
801
+ Returns a dict describing what was transferred, for the report.
802
+ """
803
+ loser_number = loser.get("number")
804
+ owner, name = repo.split("/", 1)
805
+ transfer_log = {
806
+ "loser": loser_number,
807
+ "keeper": keeper_number,
808
+ "assignees_transferred": [],
809
+ "sub_issues_reparented": [],
810
+ "sub_issue_reparent_failures": [],
811
+ }
812
+
813
+ if loser_number is None:
814
+ return transfer_log
815
+
816
+ # 1. Assignees
817
+ try:
818
+ loser_assignees = get_issue_assignees(loser_number, repo)
819
+ except RuntimeError:
820
+ loser_assignees = []
821
+ if loser_assignees:
822
+ add_assignees(keeper_number, repo, loser_assignees, dry_run=dry_run)
823
+ transfer_log["assignees_transferred"] = loser_assignees
824
+
825
+ # 2. Sub-issues
826
+ sub_numbers = list_sub_issues(owner, name, loser_number)
827
+ for sub_num in sub_numbers:
828
+ if reparent_sub_issue(owner, name, sub_num, keeper_number):
829
+ transfer_log["sub_issues_reparented"].append(sub_num)
830
+ else:
831
+ transfer_log["sub_issue_reparent_failures"].append(sub_num)
832
+
833
+ # 3 + 4: comment and close
834
+ today = date.today().isoformat()
835
+ transferred_summary_parts = []
836
+ if transfer_log["assignees_transferred"]:
837
+ transferred_summary_parts.append(
838
+ f"Transferred {len(transfer_log['assignees_transferred'])} assignee(s)"
839
+ )
840
+ if transfer_log["sub_issues_reparented"]:
841
+ transferred_summary_parts.append(
842
+ f"moved {len(transfer_log['sub_issues_reparented'])} sub-issue(s)"
843
+ )
844
+ transferred_summary = "; ".join(transferred_summary_parts) if transferred_summary_parts else "No state to transfer"
845
+
846
+ gh(
847
+ [
848
+ "issue", "comment", str(loser_number),
849
+ "--repo", repo,
850
+ "--body",
851
+ f"Duplicate of #{keeper_number} — closed by registry sync on {today}. "
852
+ f"{transferred_summary}.",
853
+ ],
854
+ dry_run=dry_run,
855
+ )
856
+ gh(
857
+ [
858
+ "issue", "close", str(loser_number),
859
+ "--repo", repo,
860
+ "--reason", "not planned",
861
+ ],
862
+ dry_run=dry_run,
863
+ )
864
+
865
+ return transfer_log
866
+
867
+
868
+ def close_duplicate(issue_number: int, keep_number: int, repo: str, dry_run: bool) -> None:
869
+ """Legacy shim — kept for backwards compatibility with any caller
870
+ that doesn't need the full conflation. Prefer conflate_duplicate."""
871
+ today = date.today().isoformat()
872
+ gh(
873
+ [
874
+ "issue", "comment", str(issue_number),
875
+ "--repo", repo,
876
+ "--body", f"Duplicate of #{keep_number} — closed by registry sync on {today}.",
877
+ ],
878
+ dry_run=dry_run,
879
+ )
880
+ gh(
881
+ [
882
+ "issue", "close", str(issue_number),
883
+ "--repo", repo,
884
+ "--reason", "not planned",
885
+ ],
886
+ dry_run=dry_run,
887
+ )
888
+
889
+
890
+ def pick_survivor(issues: List[dict]) -> dict:
891
+ """Duplicate elimination: issue with sub-issues wins; tiebreak on lowest number.
892
+
893
+ Rationale: sub-issues (bug tracking, review fixes) represent
894
+ accumulated work history. Closing a parent with live sub-issues
895
+ would orphan them.
896
+ """
897
+ with_subs = [i for i in issues if i.get("sub_issue_count", 0) > 0]
898
+ pool = with_subs if with_subs else issues
899
+ return min(pool, key=lambda d: d.get("number", 0))
900
+
901
+
902
+ # --------------------------------------------------------------------
903
+ # GitHub operations — GraphQL (project fields)
904
+ # --------------------------------------------------------------------
905
+
906
+ def find_project_item_id(
907
+ owner: str,
908
+ name: str,
909
+ issue_number: int,
910
+ project_id: str = PROJECT_ID,
911
+ ) -> Optional[str]:
912
+ """Return the ProjectV2Item ID for a given issue, or None if not in project."""
913
+ q = """
914
+ query($owner: String!, $name: String!, $number: Int!) {
915
+ repository(owner: $owner, name: $name) {
916
+ issue(number: $number) {
917
+ projectItems(first: 20) {
918
+ nodes {
919
+ id
920
+ project { id }
921
+ }
922
+ }
923
+ }
924
+ }
925
+ }
926
+ """
927
+ data = gh_graphql(q, {"owner": owner, "name": name, "number": issue_number})
928
+ try:
929
+ items = data["repository"]["issue"]["projectItems"]["nodes"]
930
+ except (KeyError, TypeError):
931
+ return None
932
+ for item in items or []:
933
+ if (item.get("project") or {}).get("id") == project_id:
934
+ return item.get("id")
935
+ return None
936
+
937
+
938
+ def add_issue_to_project(
939
+ issue_node_id: str,
940
+ project_id: str = PROJECT_ID,
941
+ ) -> Optional[str]:
942
+ """Add an issue to the project. Returns the new ProjectV2Item ID."""
943
+ q = """
944
+ mutation($projectId: ID!, $contentId: ID!) {
945
+ addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
946
+ item { id }
947
+ }
948
+ }
949
+ """
950
+ data = gh_graphql(q, {"projectId": project_id, "contentId": issue_node_id})
951
+ try:
952
+ return data["addProjectV2ItemById"]["item"]["id"]
953
+ except (KeyError, TypeError):
954
+ return None
955
+
956
+
957
+ def get_issue_node_id(owner: str, name: str, issue_number: int) -> str:
958
+ q = """
959
+ query($owner: String!, $name: String!, $number: Int!) {
960
+ repository(owner: $owner, name: $name) {
961
+ issue(number: $number) { id }
962
+ }
963
+ }
964
+ """
965
+ data = gh_graphql(q, {"owner": owner, "name": name, "number": issue_number})
966
+ return data["repository"]["issue"]["id"]
967
+
968
+
969
+ def set_project_field(
970
+ project_id: str,
971
+ item_id: str,
972
+ field_id: str,
973
+ option_id: str,
974
+ ) -> None:
975
+ q = """
976
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
977
+ updateProjectV2ItemFieldValue(input: {
978
+ projectId: $projectId,
979
+ itemId: $itemId,
980
+ fieldId: $fieldId,
981
+ value: { singleSelectOptionId: $optionId }
982
+ }) { projectV2Item { id } }
983
+ }
984
+ """
985
+ gh_graphql(q, {
986
+ "projectId": project_id,
987
+ "itemId": item_id,
988
+ "fieldId": field_id,
989
+ "optionId": option_id,
990
+ })
991
+
992
+
993
+ def sync_project_fields(
994
+ item_id: str,
995
+ flag_states: dict,
996
+ project_id: str = PROJECT_ID,
997
+ status_column: Optional[str] = None,
998
+ review_progress_n: Optional[int] = None,
999
+ ) -> dict:
1000
+ """Apply the 4 flag-state values + Status + Review Progress to a project item.
1001
+
1002
+ Each field mutation is wrapped in its own try/except so a single
1003
+ hanging or failing call doesn't skip the others. Per-field errors
1004
+ are captured in the returned dict as `{flag}:error=<msg>` so the
1005
+ caller can log them.
1006
+
1007
+ If `status_column` is None, the Status field is left untouched.
1008
+ If `review_progress_n` is None, the Review Progress field is left untouched.
1009
+ """
1010
+ applied: dict = {}
1011
+
1012
+ # 4 flag fields
1013
+ for flag, state in flag_states.items():
1014
+ spec = FLAG_FIELDS.get(flag)
1015
+ if not spec:
1016
+ continue
1017
+ option_id = spec["options"].get(state)
1018
+ if not option_id:
1019
+ continue
1020
+ try:
1021
+ set_project_field(project_id, item_id, spec["field_id"], option_id)
1022
+ applied[flag] = state
1023
+ except RuntimeError as e:
1024
+ applied[f"{flag}:error"] = str(e)[:200]
1025
+
1026
+ # Status column (kanban)
1027
+ if status_column is not None:
1028
+ opt = STATUS_OPTIONS.get(status_column)
1029
+ if opt:
1030
+ try:
1031
+ set_project_field(project_id, item_id, STATUS_FIELD_ID, opt)
1032
+ applied["status"] = status_column
1033
+ except RuntimeError as e:
1034
+ applied["status:error"] = str(e)[:200]
1035
+
1036
+ # Review Progress (replaces review:N/4 labels)
1037
+ if review_progress_n is not None:
1038
+ name = review_progress_option_name(review_progress_n)
1039
+ opt = REVIEW_PROGRESS_OPTIONS.get(name)
1040
+ if opt:
1041
+ try:
1042
+ set_project_field(project_id, item_id, REVIEW_PROGRESS_FIELD_ID, opt)
1043
+ applied["review_progress"] = name
1044
+ except RuntimeError as e:
1045
+ applied["review_progress:error"] = str(e)[:200]
1046
+
1047
+ return applied
1048
+
1049
+
1050
+ # --------------------------------------------------------------------
1051
+ # Flag labels (at-a-glance visibility in the issue list)
1052
+ # --------------------------------------------------------------------
1053
+
1054
+ def compute_flag_labels_from_states(flag_states: dict) -> List[str]:
1055
+ """Given {flag: state}, return the flag labels that should be applied.
1056
+
1057
+ Label present = flag is green (done and current).
1058
+ Label absent = flag is gray (not done) OR yellow (stale).
1059
+
1060
+ The yellow state is intentionally invisible at the label layer —
1061
+ it's tracked on the Projects V2 fields so the issue list only
1062
+ shows completed flags, not in-progress or stale ones."""
1063
+ labels: List[str] = []
1064
+ for flag, state in flag_states.items():
1065
+ name = FLAG_LABEL_NAMES.get(flag)
1066
+ if not name:
1067
+ continue
1068
+ if state == "green":
1069
+ labels.append(name)
1070
+ return sorted(labels)
1071
+
1072
+
1073
+ def sync_flag_labels(
1074
+ issue_number: int,
1075
+ repo: str,
1076
+ current_labels: List[str],
1077
+ desired_labels: List[str],
1078
+ dry_run: bool,
1079
+ ) -> dict:
1080
+ """Apply desired flag labels to an issue, remove any drifted ones
1081
+ and any legacy phase:* labels. Only touches managed labels —
1082
+ team/component/review/etc. are untouched."""
1083
+ current_set = set(current_labels)
1084
+ desired_set = set(desired_labels)
1085
+ managed = set(ALL_FLAG_LABELS) | set(LEGACY_PHASE_LABELS)
1086
+
1087
+ to_add = sorted(desired_set - current_set)
1088
+ to_remove = sorted((current_set & managed) - desired_set)
1089
+
1090
+ if not to_add and not to_remove:
1091
+ return {"action": "noop", "added": [], "removed": []}
1092
+
1093
+ args = ["issue", "edit", str(issue_number), "--repo", repo]
1094
+ if to_remove:
1095
+ args += ["--remove-label", ",".join(to_remove)]
1096
+ if to_add:
1097
+ args += ["--add-label", ",".join(to_add)]
1098
+ gh(args, dry_run=dry_run)
1099
+ return {"action": "updated", "added": to_add, "removed": to_remove}
1100
+
1101
+
1102
+ # --------------------------------------------------------------------
1103
+ # Issue body for new issues
1104
+ # --------------------------------------------------------------------
1105
+
1106
+ def build_missing_issue_body(entry: dict, flag_states: dict) -> str:
1107
+ node_id = entry.get("nodeId", "")
1108
+ file_key = entry.get("fileKey", "")
1109
+ cn = entry.get("componentName")
1110
+ if isinstance(cn, dict):
1111
+ component_name = cn.get("ios") or cn.get("android") or "Unknown"
1112
+ elif isinstance(cn, str):
1113
+ component_name = cn
1114
+ else:
1115
+ component_name = "Unknown"
1116
+
1117
+ figma_url = (
1118
+ f"https://www.figma.com/design/{file_key}/?node-id={node_id.replace(':', '-')}"
1119
+ if file_key
1120
+ else "_No response_"
1121
+ )
1122
+ table = build_progress_table(flag_states)
1123
+
1124
+ return (
1125
+ f"### Figma URL\n\n{figma_url}\n\n"
1126
+ f"### Node ID\n\n{node_id}\n\n"
1127
+ f"### Registry Name\n\n{component_name}\n\n"
1128
+ f"### Component Name (PascalCase)\n\n{component_name}\n\n"
1129
+ f"### Figma Page\n\nOther\n\n"
1130
+ f"### Jira Issue\n\n_No response_\n\n"
1131
+ f"### Progress\n\n{table}\n"
1132
+ f"### Acceptance Criteria\n\n_No response_\n\n"
1133
+ f"### Pull Requests\n\n- **common:**\n- **uicomponents:**\n\n"
1134
+ f"### Notes\n\n_Auto-created by registry sync on {date.today().isoformat()}._\n"
1135
+ )
1136
+
1137
+
1138
+ def create_missing_issue(
1139
+ entry: dict,
1140
+ flag_states: dict,
1141
+ repo: str,
1142
+ team_label: str,
1143
+ dry_run: bool,
1144
+ ) -> Optional[int]:
1145
+ cn = entry.get("componentName")
1146
+ if isinstance(cn, dict):
1147
+ name = cn.get("ios") or cn.get("android") or "Unknown"
1148
+ else:
1149
+ name = cn if isinstance(cn, str) else "Unknown"
1150
+
1151
+ title = f"feat: Implement {name} component"
1152
+ body = build_missing_issue_body(entry, flag_states)
1153
+
1154
+ args = [
1155
+ "issue", "create",
1156
+ "--repo", repo,
1157
+ "--title", title,
1158
+ "--body", body,
1159
+ "--label", "component",
1160
+ "--label", "redesign",
1161
+ "--label", team_label,
1162
+ ]
1163
+ # Flag labels from computed states
1164
+ for fl in compute_flag_labels_from_states(flag_states):
1165
+ args += ["--label", fl]
1166
+
1167
+ out = gh(args, dry_run=dry_run)
1168
+ if dry_run or not out:
1169
+ return None
1170
+
1171
+ m = re.search(r"/issues/(\d+)", out)
1172
+ return int(m.group(1)) if m else None
1173
+
1174
+
1175
+ def rewrite_issue_body(issue_number: int, repo: str, new_body: str, dry_run: bool) -> None:
1176
+ gh(
1177
+ ["issue", "edit", str(issue_number), "--repo", repo, "--body", new_body],
1178
+ dry_run=dry_run,
1179
+ )
1180
+
1181
+
1182
+ def get_issue_body(issue_number: int, repo: str) -> str:
1183
+ out = gh(["issue", "view", str(issue_number), "--repo", repo, "--json", "body"])
1184
+ try:
1185
+ return (json.loads(out) or {}).get("body", "")
1186
+ except json.JSONDecodeError:
1187
+ return ""
1188
+
1189
+
1190
+ # --------------------------------------------------------------------
1191
+ # Orchestration
1192
+ # --------------------------------------------------------------------
1193
+
1194
+ def sync_one(
1195
+ registry_dir: Path,
1196
+ node_id: str,
1197
+ platform: str,
1198
+ repo: str,
1199
+ team_label: str,
1200
+ dry_run: bool,
1201
+ project_id: str = PROJECT_ID,
1202
+ ) -> dict:
1203
+ entry = load_entry(registry_dir, node_id)
1204
+ if entry is None:
1205
+ return {"status": "error", "reason": f"registry entry missing for {node_id}"}
1206
+
1207
+ if entry.get("skip"):
1208
+ return {"status": "ok", "action": "skip_marked_skip", "node_id": node_id}
1209
+
1210
+ if is_icon_entry(entry):
1211
+ return {"status": "ok", "action": "skip_icon", "node_id": node_id}
1212
+
1213
+ flag_states = compute_flag_states(entry, platform)
1214
+ implemented_by = get_implemented_by(entry, platform)
1215
+ blocked = is_blocked(node_id, registry_dir, platform)
1216
+
1217
+ if dry_run:
1218
+ # Column + reviews can only be computed after we have the issue in
1219
+ # hand (needs label list + assignees). Compute an approximate column
1220
+ # assuming no labels + no assignees for the dry-run preview.
1221
+ preview_column = compute_status_column(
1222
+ flag_states=flag_states,
1223
+ label_names=[],
1224
+ has_assignees=bool(implemented_by),
1225
+ blocked=blocked,
1226
+ )
1227
+ return {
1228
+ "status": "ok",
1229
+ "dry_run": True,
1230
+ "node_id": node_id,
1231
+ "flag_states": flag_states,
1232
+ "implemented_by": implemented_by,
1233
+ "blocked": blocked,
1234
+ "preview_status_column": preview_column,
1235
+ "progress_table": build_progress_table(flag_states).splitlines(),
1236
+ }
1237
+
1238
+ issues = find_all_open_issues(node_id, repo, dry_run=False)
1239
+ owner, name = repo.split("/", 1)
1240
+
1241
+ result: dict = {
1242
+ "status": "ok",
1243
+ "node_id": node_id,
1244
+ "flag_states": flag_states,
1245
+ }
1246
+
1247
+ # --- Zero issues: create ---------------------------------------
1248
+ if not issues:
1249
+ new_num = create_missing_issue(
1250
+ entry=entry,
1251
+ flag_states=flag_states,
1252
+ repo=repo,
1253
+ team_label=team_label,
1254
+ dry_run=False,
1255
+ )
1256
+ if new_num is None:
1257
+ result["action"] = "create_failed"
1258
+ return result
1259
+
1260
+ # Assignee from implementedBy (best-effort)
1261
+ if implemented_by:
1262
+ add_assignees(new_num, repo, [implemented_by], dry_run=False)
1263
+ result["assignee"] = implemented_by
1264
+
1265
+ # Compute status column for the freshly created issue. It has at most
1266
+ # one assignee (implemented_by) and zero labels at this point — so
1267
+ # bugfix/review-count inputs are empty.
1268
+ new_status_column = compute_status_column(
1269
+ flag_states=flag_states,
1270
+ label_names=[],
1271
+ has_assignees=bool(implemented_by),
1272
+ blocked=blocked,
1273
+ )
1274
+ new_review_n = 0 # fresh issue, no reviews yet
1275
+
1276
+ # Add to project, then set fields
1277
+ try:
1278
+ issue_node_id = get_issue_node_id(owner, name, new_num)
1279
+ item_id = add_issue_to_project(issue_node_id, project_id) or find_project_item_id(owner, name, new_num, project_id)
1280
+ if item_id:
1281
+ applied = sync_project_fields(
1282
+ item_id,
1283
+ flag_states,
1284
+ project_id,
1285
+ status_column=new_status_column,
1286
+ review_progress_n=new_review_n,
1287
+ )
1288
+ result["project_fields"] = applied
1289
+ except RuntimeError as e:
1290
+ result["project_fields_error"] = str(e)
1291
+
1292
+ result["action"] = "created"
1293
+ result["issue_number"] = new_num
1294
+ result["status_column"] = new_status_column
1295
+ return result
1296
+
1297
+ # --- Multiple issues: duplicate rule with full conflation ------
1298
+ conflation_log: List[dict] = []
1299
+ if len(issues) > 1:
1300
+ keep = pick_survivor(issues)
1301
+ for dup in issues:
1302
+ if dup.get("number") == keep.get("number"):
1303
+ continue
1304
+ transfer_log = conflate_duplicate(dup, keep["number"], repo, dry_run=False)
1305
+ conflation_log.append(transfer_log)
1306
+ result["closed_duplicates"] = [c["loser"] for c in conflation_log]
1307
+ result["conflation"] = conflation_log
1308
+ survivor = keep
1309
+ else:
1310
+ survivor = issues[0]
1311
+ result["closed_duplicates"] = []
1312
+
1313
+ keep_number = survivor["number"]
1314
+ result["issue_number"] = keep_number
1315
+ result["sub_issue_count"] = survivor.get("sub_issue_count", 0)
1316
+
1317
+ # --- Sync the survivor -----------------------------------------
1318
+ new_body = rewrite_progress_table(survivor.get("body", ""), flag_states)
1319
+ if new_body != survivor.get("body", ""):
1320
+ rewrite_issue_body(keep_number, repo, new_body, dry_run=False)
1321
+ result["body"] = "rewritten"
1322
+ else:
1323
+ result["body"] = "unchanged"
1324
+
1325
+ # Flag labels (at-a-glance visibility in the issue list)
1326
+ current_label_names = [
1327
+ lbl.get("name", "") for lbl in survivor.get("labels", []) if isinstance(lbl, dict)
1328
+ ]
1329
+ desired_flag_labels = compute_flag_labels_from_states(flag_states)
1330
+ label_result = sync_flag_labels(
1331
+ issue_number=keep_number,
1332
+ repo=repo,
1333
+ current_labels=current_label_names,
1334
+ desired_labels=desired_flag_labels,
1335
+ dry_run=False,
1336
+ )
1337
+ result["label_diff"] = label_result
1338
+
1339
+ # Assignee from implementedBy — additive, never removes existing assignees
1340
+ if implemented_by:
1341
+ try:
1342
+ current_assignees = get_issue_assignees(keep_number, repo)
1343
+ except RuntimeError:
1344
+ current_assignees = []
1345
+ if implemented_by not in current_assignees:
1346
+ add_assignees(keep_number, repo, [implemented_by], dry_run=False)
1347
+ result["assignee_added"] = implemented_by
1348
+ else:
1349
+ result["assignee_noop"] = implemented_by
1350
+
1351
+ # --- Bug sub-issue count (for Bugfix column + label cleanup) -----
1352
+ open_bugs = 0
1353
+ try:
1354
+ open_bugs = count_open_bug_sub_issues(owner, name, keep_number)
1355
+ except RuntimeError:
1356
+ pass
1357
+ result["open_bug_sub_issues"] = open_bugs
1358
+
1359
+ # Auto-remove stale review-fix label: if no open bug sub-issues
1360
+ # remain but the parent still carries review-fix, strip it so the
1361
+ # component can exit the Bugfix column.
1362
+ if open_bugs == 0 and "review-fix" in current_label_names:
1363
+ try:
1364
+ gh(["issue", "edit", str(keep_number), "--repo", repo,
1365
+ "--remove-label", "review-fix"])
1366
+ current_label_names = [l for l in current_label_names if l != "review-fix"]
1367
+ result["review_fix_label"] = "auto_removed"
1368
+ except RuntimeError:
1369
+ result["review_fix_label"] = "remove_failed"
1370
+
1371
+ # --- Effective review progress (stale-aware) ----------------------
1372
+ review_n = effective_review_progress(current_label_names, flag_states)
1373
+ raw_review_n = parse_review_progress_from_labels(current_label_names)
1374
+
1375
+ # If effective reviews differ from raw (stale), strip old review label
1376
+ # and set review:0/4 so labels match the effective state.
1377
+ if review_n != raw_review_n and raw_review_n > 0:
1378
+ stale_label = f"review:{raw_review_n}/4"
1379
+ try:
1380
+ args_remove = ["issue", "edit", str(keep_number), "--repo", repo,
1381
+ "--remove-label", stale_label,
1382
+ "--add-label", "review:0/4"]
1383
+ gh(args_remove)
1384
+ current_label_names = [l for l in current_label_names if l != stale_label] + ["review:0/4"]
1385
+ result["review_reset"] = f"{stale_label} -> review:0/4 (stale)"
1386
+ except RuntimeError:
1387
+ result["review_reset"] = f"failed to reset {stale_label}"
1388
+
1389
+ # --- Compute status column ----------------------------------------
1390
+ try:
1391
+ current_assignees_for_col = get_issue_assignees(keep_number, repo)
1392
+ except RuntimeError:
1393
+ current_assignees_for_col = []
1394
+ has_assignees_now = bool(current_assignees_for_col) or bool(implemented_by)
1395
+
1396
+ status_column = compute_status_column(
1397
+ flag_states=flag_states,
1398
+ label_names=current_label_names,
1399
+ has_assignees=has_assignees_now,
1400
+ blocked=blocked,
1401
+ open_bug_sub_issues=open_bugs,
1402
+ )
1403
+ result["status_column"] = status_column
1404
+ result["review_progress"] = review_progress_option_name(review_n)
1405
+ result["blocked"] = blocked
1406
+
1407
+ try:
1408
+ item_id = find_project_item_id(owner, name, keep_number, project_id)
1409
+ if not item_id:
1410
+ # Survivor isn't on the project — add it
1411
+ issue_node_id = get_issue_node_id(owner, name, keep_number)
1412
+ item_id = add_issue_to_project(issue_node_id, project_id)
1413
+ if item_id:
1414
+ applied = sync_project_fields(
1415
+ item_id,
1416
+ flag_states,
1417
+ project_id,
1418
+ status_column=status_column,
1419
+ review_progress_n=review_n,
1420
+ )
1421
+ result["project_fields"] = applied
1422
+ else:
1423
+ result["project_fields_error"] = "could not resolve project item"
1424
+ except RuntimeError as e:
1425
+ result["project_fields_error"] = str(e)
1426
+
1427
+ result["action"] = "synced"
1428
+ return result
1429
+
1430
+
1431
+ # --------------------------------------------------------------------
1432
+ # CLI
1433
+ # --------------------------------------------------------------------
1434
+
1435
+ def main() -> int:
1436
+ parser = argparse.ArgumentParser(
1437
+ description="Sync one node's GitHub issue to the component registry state",
1438
+ )
1439
+ parser.add_argument("--node-id", required=True)
1440
+ parser.add_argument("--platform", default="ios", choices=["ios", "android"])
1441
+ parser.add_argument("--registry-dir")
1442
+ parser.add_argument("--repo", default=DEFAULT_REPO)
1443
+ parser.add_argument("--team-label", default=DEFAULT_TEAM_LABEL)
1444
+ parser.add_argument("--dry-run", action="store_true")
1445
+ args = parser.parse_args()
1446
+
1447
+ registry_dir = Path(args.registry_dir) if args.registry_dir else default_registry_dir()
1448
+ if not registry_dir.exists():
1449
+ print(f"ERROR: registry directory does not exist: {registry_dir}", file=sys.stderr)
1450
+ return 1
1451
+
1452
+ try:
1453
+ result = sync_one(
1454
+ registry_dir=registry_dir,
1455
+ node_id=args.node_id,
1456
+ platform=args.platform,
1457
+ repo=args.repo,
1458
+ team_label=args.team_label,
1459
+ dry_run=args.dry_run,
1460
+ )
1461
+ except RuntimeError as e:
1462
+ print(f"ERROR: {e}", file=sys.stderr)
1463
+ return 1
1464
+
1465
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1466
+ return 0 if result.get("status") == "ok" else 1
1467
+
1468
+
1469
+ if __name__ == "__main__":
1470
+ sys.exit(main())