@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.
- package/CHANGELOG.md +2623 -0
- package/LICENSE +21 -0
- package/README.md +852 -0
- package/docs/FIGMA_PIPELINE.md +138 -0
- package/docs/GENERICITY-REVIEW.md +277 -0
- package/docs/STABILITY-FIX-PLAN.md +168 -0
- package/docs/adr/0001-three-model-triage.md +81 -0
- package/docs/adr/0002-instruction-driven-flag.md +62 -0
- package/docs/adr/0003-unified-shared-skills.md +55 -0
- package/docs/adr/0004-zero-dependency-philosophy.md +60 -0
- package/docs/adr/0005-lazy-phase-docs.md +68 -0
- package/docs/adr/0006-skills-core-external-split.md +52 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +110 -0
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +98 -0
- package/docs/adr/README.md +33 -0
- package/docs/architecture.md +181 -0
- package/docs/best-practices.md +93 -0
- package/docs/features.md +274 -0
- package/docs/performance.md +116 -0
- package/docs/recovery-guide.md +479 -0
- package/index.js +76 -0
- package/install/_adapters.mjs +69 -0
- package/install/_common.mjs +150 -0
- package/install/_copilot-instructions.mjs +32 -0
- package/install/_dev-only-files.mjs +23 -0
- package/install/_platform-filter.mjs +132 -0
- package/install/_telemetry.mjs +79 -0
- package/install/claude.mjs +332 -0
- package/install/copilot.mjs +254 -0
- package/install/index.mjs +179 -0
- package/install/templates/copilot-instructions.md +319 -0
- package/install.js +24 -0
- package/package.json +78 -0
- package/pipeline/adapters/_base.mjs +288 -0
- package/pipeline/adapters/copilot-chat.mjs +158 -0
- package/pipeline/adapters/cursor.mjs +187 -0
- package/pipeline/agents/android-architect.md +42 -0
- package/pipeline/agents/backend-architect.md +43 -0
- package/pipeline/agents/code-reviewer.md +57 -0
- package/pipeline/agents/dev-critic.md +148 -0
- package/pipeline/agents/explorer.md +34 -0
- package/pipeline/agents/ios-architect.md +41 -0
- package/pipeline/agents/security-auditor.md +98 -0
- package/pipeline/agents/task-clarifier.md +113 -0
- package/pipeline/claude-md-template.md +55 -0
- package/pipeline/commands/archive-guard.md +45 -0
- package/pipeline/commands/deploy.md +54 -0
- package/pipeline/commands/figma-to-swiftui.md +295 -0
- package/pipeline/commands/multi-agent/_account-picker.md +90 -0
- package/pipeline/commands/multi-agent/_dev-context.md +111 -0
- package/pipeline/commands/multi-agent/_input-parser.md +43 -0
- package/pipeline/commands/multi-agent/_repo-picker.md +76 -0
- package/pipeline/commands/multi-agent/autopilot.md +116 -0
- package/pipeline/commands/multi-agent/channels.md +465 -0
- package/pipeline/commands/multi-agent/delete.md +66 -0
- package/pipeline/commands/multi-agent/dev-autopilot.md +120 -0
- package/pipeline/commands/multi-agent/dev-local-autopilot.md +110 -0
- package/pipeline/commands/multi-agent/dev-local.md +105 -0
- package/pipeline/commands/multi-agent/dev.md +246 -0
- package/pipeline/commands/multi-agent/diff-explain.md +68 -0
- package/pipeline/commands/multi-agent/help.md +422 -0
- package/pipeline/commands/multi-agent/issue.md +79 -0
- package/pipeline/commands/multi-agent/jira.md +132 -0
- package/pipeline/commands/multi-agent/kill.md +38 -0
- package/pipeline/commands/multi-agent/language.md +94 -0
- package/pipeline/commands/multi-agent/local-autopilot.md +139 -0
- package/pipeline/commands/multi-agent/local.md +117 -0
- package/pipeline/commands/multi-agent/log.md +25 -0
- package/pipeline/commands/multi-agent/manual-test.md +43 -0
- package/pipeline/commands/multi-agent/purge.md +39 -0
- package/pipeline/commands/multi-agent/refactor.md +188 -0
- package/pipeline/commands/multi-agent/refs/android-guide.md +250 -0
- package/pipeline/commands/multi-agent/refs/audit-guide.md +240 -0
- package/pipeline/commands/multi-agent/refs/backend-guide.md +135 -0
- package/pipeline/commands/multi-agent/refs/channels/confluence.md +153 -0
- package/pipeline/commands/multi-agent/refs/channels/issue-comment.md +141 -0
- package/pipeline/commands/multi-agent/refs/channels/jira.md +127 -0
- package/pipeline/commands/multi-agent/refs/channels/pr-review-actions.md +135 -0
- package/pipeline/commands/multi-agent/refs/channels/pr.md +139 -0
- package/pipeline/commands/multi-agent/refs/channels/wiki.md +66 -0
- package/pipeline/commands/multi-agent/refs/component-dispatch.md +92 -0
- package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +326 -0
- package/pipeline/commands/multi-agent/refs/frontend-guide.md +136 -0
- package/pipeline/commands/multi-agent/refs/issue-jira-triad.md +104 -0
- package/pipeline/commands/multi-agent/refs/keychain.md +80 -0
- package/pipeline/commands/multi-agent/refs/knowledge.md +112 -0
- package/pipeline/commands/multi-agent/refs/multi-repo-integration-build.md +207 -0
- package/pipeline/commands/multi-agent/refs/phases/log-format.md +89 -0
- package/pipeline/commands/multi-agent/refs/phases/modes.md +156 -0
- package/pipeline/commands/multi-agent/refs/phases/operations.md +91 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-0-init.md +481 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-1-analysis.md +264 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-2-planning.md +278 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-3-dev.md +364 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +378 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-5-test.md +129 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-6-commit.md +339 -0
- package/pipeline/commands/multi-agent/refs/phases/phase-7-report.md +361 -0
- package/pipeline/commands/multi-agent/refs/phases.md +187 -0
- package/pipeline/commands/multi-agent/refs/progress-contract.md +155 -0
- package/pipeline/commands/multi-agent/refs/rules.md +189 -0
- package/pipeline/commands/multi-agent/refs/swiftui-guide.md +254 -0
- package/pipeline/commands/multi-agent/refs/tracker-contract.md +256 -0
- package/pipeline/commands/multi-agent/refs/wiki-capture.md +109 -0
- package/pipeline/commands/multi-agent/resume.md +28 -0
- package/pipeline/commands/multi-agent/review.md +228 -0
- package/pipeline/commands/multi-agent/scan.md +74 -0
- package/pipeline/commands/multi-agent/search.md +97 -0
- package/pipeline/commands/multi-agent/setup.md +767 -0
- package/pipeline/commands/multi-agent/stack.md +48 -0
- package/pipeline/commands/multi-agent/status.md +38 -0
- package/pipeline/commands/multi-agent/sync.md +319 -0
- package/pipeline/commands/multi-agent/test.md +39 -0
- package/pipeline/commands/multi-agent/update.md +88 -0
- package/pipeline/commands/multi-agent.md +293 -0
- package/pipeline/commands/security-review.md +6 -0
- package/pipeline/commands/sim-test.md +256 -0
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-1-analysis.json +25 -0
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-2-plan.json +30 -0
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-review.json +20 -0
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-triage.json +15 -0
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/metadata.json +14 -0
- package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/task.json +12 -0
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-1-analysis.json +29 -0
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-2-plan.json +43 -0
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-review.json +35 -0
- package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-triage.json +35 -0
- package/pipeline/eval/golden-tasks/02-android-feature-compose/metadata.json +14 -0
- package/pipeline/eval/golden-tasks/02-android-feature-compose/task.json +12 -0
- package/pipeline/eval/golden-tasks/README.md +65 -0
- package/pipeline/eval/triage/01-empty-findings/expected.json +6 -0
- package/pipeline/eval/triage/01-empty-findings/input.json +5 -0
- package/pipeline/eval/triage/01-empty-findings/notes.md +7 -0
- package/pipeline/eval/triage/02-real-blocker/expected.json +15 -0
- package/pipeline/eval/triage/02-real-blocker/input.json +14 -0
- package/pipeline/eval/triage/02-real-blocker/notes.md +7 -0
- package/pipeline/eval/triage/03-out-of-scope-defer/expected.json +18 -0
- package/pipeline/eval/triage/03-out-of-scope-defer/input.json +14 -0
- package/pipeline/eval/triage/03-out-of-scope-defer/notes.md +10 -0
- package/pipeline/eval/triage/04-false-positive-reject/expected.json +18 -0
- package/pipeline/eval/triage/04-false-positive-reject/input.json +14 -0
- package/pipeline/eval/triage/04-false-positive-reject/notes.md +10 -0
- package/pipeline/eval/triage/05-mixed-classification/expected.json +43 -0
- package/pipeline/eval/triage/05-mixed-classification/input.json +38 -0
- package/pipeline/eval/triage/05-mixed-classification/notes.md +17 -0
- package/pipeline/eval/triage/06-severity-mismatch/expected.json +15 -0
- package/pipeline/eval/triage/06-severity-mismatch/input.json +14 -0
- package/pipeline/eval/triage/06-severity-mismatch/notes.md +9 -0
- package/pipeline/eval/triage/07-duplicate-reviewers/expected.json +27 -0
- package/pipeline/eval/triage/07-duplicate-reviewers/input.json +22 -0
- package/pipeline/eval/triage/07-duplicate-reviewers/notes.md +9 -0
- package/pipeline/eval/triage/08-style-misclassified/expected.json +18 -0
- package/pipeline/eval/triage/08-style-misclassified/input.json +14 -0
- package/pipeline/eval/triage/08-style-misclassified/notes.md +9 -0
- package/pipeline/eval/triage/09-cascading-finding/expected.json +23 -0
- package/pipeline/eval/triage/09-cascading-finding/input.json +22 -0
- package/pipeline/eval/triage/09-cascading-finding/notes.md +9 -0
- package/pipeline/eval/triage/10-deferred-crossref/expected.json +18 -0
- package/pipeline/eval/triage/10-deferred-crossref/input.json +14 -0
- package/pipeline/eval/triage/10-deferred-crossref/notes.md +9 -0
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/expected.json +27 -0
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/input.json +22 -0
- package/pipeline/eval/triage/11-vercel-token-leak-blocker/notes.md +14 -0
- package/pipeline/eval/triage/README.md +54 -0
- package/pipeline/lib/account-resolver.sh +204 -0
- package/pipeline/lib/channels-multi-repo.sh +218 -0
- package/pipeline/lib/context-link-extractor.sh +192 -0
- package/pipeline/lib/credential-store-resolver.sh +57 -0
- package/pipeline/lib/credential-store.sh +226 -0
- package/pipeline/lib/fetch-confluence.sh +358 -0
- package/pipeline/lib/fetch-crashlytics.sh +314 -0
- package/pipeline/lib/fetch-fortify.sh +321 -0
- package/pipeline/lib/fetch-swagger.sh +270 -0
- package/pipeline/lib/issue-fetcher.sh +333 -0
- package/pipeline/lib/multi-repo-pipeline.sh +252 -0
- package/pipeline/lib/plan-todos.sh +284 -0
- package/pipeline/lib/post-pr-review.sh +374 -0
- package/pipeline/lib/repo-cache.sh +231 -0
- package/pipeline/lib/review-watch.sh +244 -0
- package/pipeline/lib/shadow-git.sh +222 -0
- package/pipeline/lib/submodule-detector.sh +177 -0
- package/pipeline/lib/vercel-deploy.sh +170 -0
- package/pipeline/preferences-template.json +132 -0
- package/pipeline/rules/app-store-guidelines.md +59 -0
- package/pipeline/rules/code-review.md +27 -0
- package/pipeline/rules/code-style.md +37 -0
- package/pipeline/rules/debugging.md +24 -0
- package/pipeline/rules/figma-pipeline.md +190 -0
- package/pipeline/rules/git-conventions.md +29 -0
- package/pipeline/rules/kotlin-android.md +92 -0
- package/pipeline/rules/performance.md +23 -0
- package/pipeline/rules/security.md +39 -0
- package/pipeline/rules/swiftui-qa.md +32 -0
- package/pipeline/rules/tdd.md +25 -0
- package/pipeline/rules/testing.md +37 -0
- package/pipeline/schemas/agent-state.schema.json +273 -0
- package/pipeline/schemas/analysis-output.schema.json +59 -0
- package/pipeline/schemas/clarify-output.schema.json +74 -0
- package/pipeline/schemas/dev-critic-output.schema.json +104 -0
- package/pipeline/schemas/diff-risk.schema.json +78 -0
- package/pipeline/schemas/figma-project-config.schema.json +372 -0
- package/pipeline/schemas/migrations/README.md +73 -0
- package/pipeline/schemas/migrations/figma-config-1.0.0-to-2.0.0.mjs +112 -0
- package/pipeline/schemas/migrations/prefs-2.0.0-to-2.1.0.mjs +75 -0
- package/pipeline/schemas/migrations/prefs-2.1.0-to-2.2.0.mjs +64 -0
- package/pipeline/schemas/migrations/prefs-2.2.0-to-2.3.0.mjs +36 -0
- package/pipeline/schemas/migrations/state-2.0.0-to-2.1.0.mjs +34 -0
- package/pipeline/schemas/plan-todos.schema.json +62 -0
- package/pipeline/schemas/planning-output.schema.json +57 -0
- package/pipeline/schemas/prefs.schema.json +1137 -0
- package/pipeline/schemas/reviewer-output.schema.json +55 -0
- package/pipeline/schemas/test-gap.schema.json +64 -0
- package/pipeline/schemas/token-budget.json +17 -0
- package/pipeline/schemas/triage-corpus.schema.json +31 -0
- package/pipeline/schemas/triage-output.schema.json +115 -0
- package/pipeline/scripts/.last-figma-sync-plan.json +23 -0
- package/pipeline/scripts/README-figma-smokes.md +34 -0
- package/pipeline/scripts/README.md +104 -0
- package/pipeline/scripts/aggregate-metrics.mjs +310 -0
- package/pipeline/scripts/audit-log-rotate.sh +61 -0
- package/pipeline/scripts/audit-log.sh +69 -0
- package/pipeline/scripts/benchmark-phase-0.sh +128 -0
- package/pipeline/scripts/build-skills-index.mjs +139 -0
- package/pipeline/scripts/classify-plan-safety.mjs +177 -0
- package/pipeline/scripts/cost-table.json +27 -0
- package/pipeline/scripts/diff-explain.mjs +276 -0
- package/pipeline/scripts/diff-risk-score.mjs +328 -0
- package/pipeline/scripts/eval-golden-tasks-live.mjs +294 -0
- package/pipeline/scripts/eval-golden-tasks.mjs +223 -0
- package/pipeline/scripts/eval-triage.mjs +171 -0
- package/pipeline/scripts/figma-placeholder-map.json +191 -0
- package/pipeline/scripts/fixtures/diff-risk-android.diff +40 -0
- package/pipeline/scripts/fixtures/diff-risk-ios.diff +48 -0
- package/pipeline/scripts/fixtures/install-layout.tsv +16 -0
- package/pipeline/scripts/fixtures/test-gap-node.diff +30 -0
- package/pipeline/scripts/fixtures/test-gap-python.diff +32 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +170 -0
- package/pipeline/scripts/gen-skills-index.mjs +90 -0
- package/pipeline/scripts/github-ssh-setup.sh +103 -0
- package/pipeline/scripts/import-figma-skills.sh +253 -0
- package/pipeline/scripts/keychain-save.sh +74 -0
- package/pipeline/scripts/keychain.py +294 -0
- package/pipeline/scripts/log-metric.sh +98 -0
- package/pipeline/scripts/match-skills.mjs +167 -0
- package/pipeline/scripts/memory-load.sh +46 -0
- package/pipeline/scripts/memory-save.sh +76 -0
- package/pipeline/scripts/migrate-prefs.mjs +390 -0
- package/pipeline/scripts/migrate-state.mjs +215 -0
- package/pipeline/scripts/output-quality-check.sh +125 -0
- package/pipeline/scripts/phase-banner.sh +158 -0
- package/pipeline/scripts/phase-tracker.sh +548 -0
- package/pipeline/scripts/pre-commit-check.sh +69 -0
- package/pipeline/scripts/pre-push-check.sh +77 -0
- package/pipeline/scripts/render-agent-log-cost.sh +149 -0
- package/pipeline/scripts/render-cost-summary.sh +137 -0
- package/pipeline/scripts/render-work-summary.sh +195 -0
- package/pipeline/scripts/repo-map.mjs +367 -0
- package/pipeline/scripts/run-aggregator.mjs +298 -0
- package/pipeline/scripts/scan-skills.sh +332 -0
- package/pipeline/scripts/search-logs.sh +291 -0
- package/pipeline/scripts/sign-skills.sh +67 -0
- package/pipeline/scripts/smoke-adapters.sh +207 -0
- package/pipeline/scripts/smoke-add-detail.sh +137 -0
- package/pipeline/scripts/smoke-agent-log-cost.sh +183 -0
- package/pipeline/scripts/smoke-agent-model-routing.sh +87 -0
- package/pipeline/scripts/smoke-bitbucket-contract.sh +223 -0
- package/pipeline/scripts/smoke-channels-flow.sh +130 -0
- package/pipeline/scripts/smoke-ci-workflows.sh +88 -0
- package/pipeline/scripts/smoke-clarify.sh +148 -0
- package/pipeline/scripts/smoke-commands-skills-parity.sh +87 -0
- package/pipeline/scripts/smoke-compliance-skills.sh +119 -0
- package/pipeline/scripts/smoke-cost-summary.sh +139 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +198 -0
- package/pipeline/scripts/smoke-cross-phase-cohesion.sh +128 -0
- package/pipeline/scripts/smoke-delete-flow.sh +151 -0
- package/pipeline/scripts/smoke-dev-critic.sh +144 -0
- package/pipeline/scripts/smoke-diff-explain.sh +128 -0
- package/pipeline/scripts/smoke-diff-risk.sh +161 -0
- package/pipeline/scripts/smoke-dynamic-skill-loading.sh +160 -0
- package/pipeline/scripts/smoke-eval-live.sh +136 -0
- package/pipeline/scripts/smoke-existing-discovery-gate.sh +71 -0
- package/pipeline/scripts/smoke-figma-android-parity.sh +148 -0
- package/pipeline/scripts/smoke-figma-config-schema.sh +144 -0
- package/pipeline/scripts/smoke-figma-credential-store.sh +105 -0
- package/pipeline/scripts/smoke-figma-cross-cli-inventory.sh +177 -0
- package/pipeline/scripts/smoke-figma-dispatch.sh +123 -0
- package/pipeline/scripts/smoke-figma-skill-import.sh +174 -0
- package/pipeline/scripts/smoke-figma-sync.sh +149 -0
- package/pipeline/scripts/smoke-identity-isolation.sh +70 -0
- package/pipeline/scripts/smoke-install-layout.sh +241 -0
- package/pipeline/scripts/smoke-install-leak-gate.sh +125 -0
- package/pipeline/scripts/smoke-issue-comment-template.sh +86 -0
- package/pipeline/scripts/smoke-issue-jira-triad.sh +120 -0
- package/pipeline/scripts/smoke-keychain.sh +158 -0
- package/pipeline/scripts/smoke-language-axis.sh +109 -0
- package/pipeline/scripts/smoke-lib-scripts.sh +395 -0
- package/pipeline/scripts/smoke-migrate-state.sh +102 -0
- package/pipeline/scripts/smoke-mode-dispatch-drift.sh +158 -0
- package/pipeline/scripts/smoke-multi-repo-integration.sh +116 -0
- package/pipeline/scripts/smoke-multi-repo-worktree.sh +61 -0
- package/pipeline/scripts/smoke-no-token-prompt.sh +69 -0
- package/pipeline/scripts/smoke-pat-audit.sh +107 -0
- package/pipeline/scripts/smoke-per-repo-memory.sh +156 -0
- package/pipeline/scripts/smoke-personal-data.sh +82 -0
- package/pipeline/scripts/smoke-phase-0-multi-repo.sh +170 -0
- package/pipeline/scripts/smoke-phase-6-multi.sh +79 -0
- package/pipeline/scripts/smoke-phase-banner.sh +101 -0
- package/pipeline/scripts/smoke-phase-tracker.sh +255 -0
- package/pipeline/scripts/smoke-phase0-bridge-contract.sh +241 -0
- package/pipeline/scripts/smoke-phase4-triage.sh +142 -0
- package/pipeline/scripts/smoke-plan-approval-gate.sh +71 -0
- package/pipeline/scripts/smoke-plan-safety.sh +139 -0
- package/pipeline/scripts/smoke-plan-todos.sh +193 -0
- package/pipeline/scripts/smoke-pr-review-actions.sh +152 -0
- package/pipeline/scripts/smoke-pre-commit.sh +138 -0
- package/pipeline/scripts/smoke-pref-migration.sh +224 -0
- package/pipeline/scripts/smoke-prefs-language.sh +134 -0
- package/pipeline/scripts/smoke-progress-contract.sh +118 -0
- package/pipeline/scripts/smoke-push-retry.sh +75 -0
- package/pipeline/scripts/smoke-readme-counts.sh +120 -0
- package/pipeline/scripts/smoke-repo-map.sh +300 -0
- package/pipeline/scripts/smoke-review-watch.sh +134 -0
- package/pipeline/scripts/smoke-run-aggregator.sh +216 -0
- package/pipeline/scripts/smoke-schema-validation.sh +173 -0
- package/pipeline/scripts/smoke-search.sh +187 -0
- package/pipeline/scripts/smoke-shadow-git.sh +175 -0
- package/pipeline/scripts/smoke-skill-authoring.sh +142 -0
- package/pipeline/scripts/smoke-skill-language.sh +83 -0
- package/pipeline/scripts/smoke-skill-manifest.sh +138 -0
- package/pipeline/scripts/smoke-skill-scan.sh +198 -0
- package/pipeline/scripts/smoke-stack-swap.sh +132 -0
- package/pipeline/scripts/smoke-subagent-validators.sh +105 -0
- package/pipeline/scripts/smoke-sync-delegation.sh +74 -0
- package/pipeline/scripts/smoke-sync-parity.sh +92 -0
- package/pipeline/scripts/smoke-tasklist-ordering.sh +111 -0
- package/pipeline/scripts/smoke-telemetry.sh +147 -0
- package/pipeline/scripts/smoke-test-gap.sh +183 -0
- package/pipeline/scripts/smoke-token-budget.sh +67 -0
- package/pipeline/scripts/smoke-tracker-contract.sh +129 -0
- package/pipeline/scripts/smoke-tracker-tokens-invocation.sh +65 -0
- package/pipeline/scripts/smoke-triage-memory.sh +174 -0
- package/pipeline/scripts/smoke-url-enrichment.sh +70 -0
- package/pipeline/scripts/smoke-validator-contradiction.sh +67 -0
- package/pipeline/scripts/smoke-vercel-deploy-redact.sh +129 -0
- package/pipeline/scripts/smoke-wiki-integration.sh +146 -0
- package/pipeline/scripts/smoke-work-summary.sh +163 -0
- package/pipeline/scripts/smoke-worktree-path-convention.sh +86 -0
- package/pipeline/scripts/smoke-write-state.sh +115 -0
- package/pipeline/scripts/stack-swap.sh +182 -0
- package/pipeline/scripts/sync-figma-source.sh +228 -0
- package/pipeline/scripts/sync-parity-check.sh +135 -0
- package/pipeline/scripts/test-gap-rules/android.json +25 -0
- package/pipeline/scripts/test-gap-rules/ios.json +29 -0
- package/pipeline/scripts/test-gap-rules/node.json +17 -0
- package/pipeline/scripts/test-gap-rules/python.json +19 -0
- package/pipeline/scripts/test-gap-scan.mjs +343 -0
- package/pipeline/scripts/token-budget-report.mjs +145 -0
- package/pipeline/scripts/triage-memory.mjs +258 -0
- package/pipeline/scripts/ui-tree-dumper.swift +122 -0
- package/pipeline/scripts/uninstall.mjs +331 -0
- package/pipeline/scripts/update-issue-progress.sh +146 -0
- package/pipeline/scripts/validate-analysis.mjs +132 -0
- package/pipeline/scripts/validate-diff-risk.mjs +117 -0
- package/pipeline/scripts/validate-planning.mjs +180 -0
- package/pipeline/scripts/validate-reviewer.mjs +131 -0
- package/pipeline/scripts/validate-schemas.mjs +88 -0
- package/pipeline/scripts/validate-test-gap.mjs +90 -0
- package/pipeline/scripts/validate-triage.mjs +175 -0
- package/pipeline/scripts/verify-skills.sh +126 -0
- package/pipeline/scripts/write-state.mjs +175 -0
- package/pipeline/skills/.skill-manifest.json +779 -0
- package/pipeline/skills/.skills-index.json +1771 -0
- package/pipeline/skills/figma-android/README.md +36 -0
- package/pipeline/skills/figma-android/figma-component-code-connect/SKILL.md +62 -0
- package/pipeline/skills/figma-android/figma-component-implement/SKILL.md +158 -0
- package/pipeline/skills/figma-android/figma-component-test/SKILL.md +120 -0
- package/pipeline/skills/figma-android/figma-component-wiki/SKILL.md +35 -0
- package/pipeline/skills/figma-android/figma-to-component/SKILL.md +124 -0
- package/pipeline/skills/figma-common/README.md +57 -0
- package/pipeline/skills/figma-common/figma-cli-iterate/SKILL.md +277 -0
- package/pipeline/skills/figma-common/figma-cli-iterate-mend/SKILL.md +498 -0
- package/pipeline/skills/figma-common/figma-cli-lean-iterate/SKILL.md +283 -0
- package/pipeline/skills/figma-common/figma-cli-skip/SKILL.md +362 -0
- package/pipeline/skills/figma-common/figma-commit/COMMON_REBASE.md +206 -0
- package/pipeline/skills/figma-common/figma-commit/REVIEW.md +337 -0
- package/pipeline/skills/figma-common/figma-commit/SKILL.md +211 -0
- package/pipeline/skills/figma-common/figma-component-confluence-sync/SKILL.md +218 -0
- package/pipeline/skills/figma-common/figma-component-start/SKILL.md +246 -0
- package/pipeline/skills/figma-common/figma-component-status-update/SKILL.md +73 -0
- package/pipeline/skills/figma-common/figma-fix/SKILL.md +316 -0
- package/pipeline/skills/figma-common/figma-form-integration/SKILL.md +542 -0
- package/pipeline/skills/figma-common/figma-issue/SKILL.md +745 -0
- package/pipeline/skills/figma-common/figma-iterate/SKILL.md +203 -0
- package/pipeline/skills/figma-common/figma-iteration-commit/SKILL.md +1015 -0
- package/pipeline/skills/figma-common/figma-mend/SKILL.md +331 -0
- package/pipeline/skills/figma-common/figma-price-integration/SKILL.md +398 -0
- package/pipeline/skills/figma-common/figma-remote-mcp-auth/SKILL.md +104 -0
- package/pipeline/skills/figma-common/figma-review/SKILL.md +395 -0
- package/pipeline/skills/figma-common/figma-setup/SKILL.md +514 -0
- package/pipeline/skills/figma-common/figma-setup/scripts/fetch-mcp-token.py +592 -0
- package/pipeline/skills/figma-common/figma-skip/SKILL.md +129 -0
- package/pipeline/skills/figma-common/figma-ui-patterns/SKILL.md +104 -0
- package/pipeline/skills/figma-common/figma-utility/SKILL.md +274 -0
- package/pipeline/skills/figma-common/figma-utility/scripts/figma-utility.py +808 -0
- package/pipeline/skills/figma-common/figma-validate/SKILL.md +633 -0
- package/pipeline/skills/figma-common/performance-iteration-commit-all/SKILL.md +711 -0
- package/pipeline/skills/figma-common/performance-review-next/SKILL.md +233 -0
- package/pipeline/skills/figma-common/performance-start/SKILL.md +425 -0
- package/pipeline/skills/figma-common/performance-swiftui/SKILL.md +706 -0
- package/pipeline/skills/figma-common/performance-tour/SKILL.md +418 -0
- package/pipeline/skills/figma-ios/REVIEW_CHECKLIST.md +67 -0
- package/pipeline/skills/figma-ios/figma-component-code-connect/SKILL.md +178 -0
- package/pipeline/skills/figma-ios/figma-component-implement/SKILL.md +184 -0
- package/pipeline/skills/figma-ios/figma-component-test/SKILL.md +219 -0
- package/pipeline/skills/figma-ios/figma-component-wiki/SKILL.md +274 -0
- package/pipeline/skills/figma-ios/figma-to-component/SKILL.md +401 -0
- package/pipeline/skills/figma-ios/figma-to-component/halt-return-protocol.md +57 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-0-init.md +307 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-1-gathering.md +119 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-1.5-existing-discovery.md +174 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2-orchestrator.md +333 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2a-testing-identifiers.md +368 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2b-localization.md +393 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2c-accessibility.md +617 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-2d-analytics.md +352 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3-orchestrator.md +337 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3a-location.md +206 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3b-tokens.md +235 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3c-nested.md +214 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3d-patterns.md +871 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3e-assets.md +156 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3f-utilities.md +175 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3g-property-coverage.md +176 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-3h-variant-config.md +333 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4-orchestrator.md +412 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4a-configuration.md +336 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4b-view.md +695 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4c-documentation.md +332 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4d-preview.md +380 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-4e-modifiers.md +262 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5-orchestrator.md +482 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5a-viewinspector.md +274 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5b-snapshot.md +636 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-5c-unit.md +142 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-6-code-connect.md +547 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7-wiki.md +39 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7a-confluence-generate.md +659 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-7a-wiki-generate.md +580 -0
- package/pipeline/skills/figma-ios/figma-to-component/phases/phase-8-cleanup.md +51 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/accessibility.md +129 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/analytics-events.md +64 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/code-connect.md +531 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/confluence-api.md +89 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/confluence-xhtml.md +155 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/figma-to-swiftui-effects.md +196 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/halt-return-protocol.md +57 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/localization-naming.md +89 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/macros.md +227 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/missing-tokens.md +157 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/orchestrator-discipline.md +90 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/registry.md +116 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/remote-mcp-script.md +153 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/rest-api-script.md +130 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/scripts-inventory.md +218 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/snapshot-testing.md +188 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/subcomponent-graph.md +93 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/testing-identifiers-naming.md +98 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/tools.md +261 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/viewinspector.md +147 -0
- package/pipeline/skills/figma-ios/figma-to-component/reference/wiki-to-confluence-mapping.md +182 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/apply-author-login-map.py +185 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/backfill-status.py +609 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/build-author-registry.py +332 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/bulk-sync-issues.py +261 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/code-connect-data-gather.py +184 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/code-connect-publish.sh +188 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-component-status-upload.py +768 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-component-status.py +191 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-data-gather.py +420 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-page-ids.json +94 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/confluence-publish.py +336 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/figma-subcomponent-graph.py +391 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/figma-update.py +292 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/__init__.py +1 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/issue_sync_propagate.py +93 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/registry_writer.py +299 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_backfill_status.py +343 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_figma_update.py +206 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_figma_update_http.py +149 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_phase_clis.py +281 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_registry_writer.py +332 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_skill_figma_issue.py +176 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_skill_figma_review.py +98 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_update_issue.py +298 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/lib/test_update_issue_gh.py +195 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase1-gather.py +1298 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase2-finalize.py +228 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase3-scripts.py +1089 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase4-finalize.py +141 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase5-finalize.py +106 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase6-finalize.py +162 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/phase7-finalize.py +105 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/register-icons-codeconnect.py +179 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/remote-mcp-fetch.py +260 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/resolve-author-logins.py +260 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/run-uicomponents-tests.sh +86 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/sidebar-generator.py +321 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/update-issue-from-registry.py +1470 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase4.sh +176 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase6.sh +147 -0
- package/pipeline/skills/figma-ios/figma-to-component/scripts/validate-phase7a.py +629 -0
- package/pipeline/skills/shared/README.md +212 -0
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +315 -0
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +348 -0
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +944 -0
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +51 -0
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +300 -0
- package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +63 -0
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +64 -0
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +56 -0
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +36 -0
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +42 -0
- package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +66 -0
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +292 -0
- package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +35 -0
- package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +38 -0
- package/pipeline/skills/shared/core/multi-agent-kill/SKILL.md +41 -0
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +87 -0
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +37 -0
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +53 -0
- package/pipeline/skills/shared/core/multi-agent-log/SKILL.md +28 -0
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +47 -0
- package/pipeline/skills/shared/core/multi-agent-purge/SKILL.md +42 -0
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +191 -0
- package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +31 -0
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +61 -0
- package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +61 -0
- package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +62 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +309 -0
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +55 -0
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +41 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +184 -0
- package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +44 -0
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +34 -0
- package/pipeline/skills/shared/external/accessibility-compliance-accessibility-audit/SKILL.md +45 -0
- package/pipeline/skills/shared/external/agentflow/SKILL.md +199 -0
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +438 -0
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +584 -0
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +407 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +153 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +736 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +577 -0
- package/pipeline/skills/shared/external/android_ui_verification/SKILL.md +66 -0
- package/pipeline/skills/shared/external/api-patterns/SKILL.md +85 -0
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +910 -0
- package/pipeline/skills/shared/external/app-clips/SKILL.md +436 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +489 -0
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +1076 -0
- package/pipeline/skills/shared/external/app-store-changelog/SKILL.md +75 -0
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +409 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +411 -0
- package/pipeline/skills/shared/external/app-store-review/references/code-signing.md +259 -0
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +90 -0
- package/pipeline/skills/shared/external/app-store-review/references/rejection-patterns.md +152 -0
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +118 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +500 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +425 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +344 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +508 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +285 -0
- package/pipeline/skills/shared/external/architecture/SKILL.md +60 -0
- package/pipeline/skills/shared/external/authentication/SKILL.md +496 -0
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +211 -0
- package/pipeline/skills/shared/external/background-processing/SKILL.md +499 -0
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +390 -0
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +461 -0
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +425 -0
- package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +462 -0
- package/pipeline/skills/shared/external/clean-code/SKILL.md +94 -0
- package/pipeline/skills/shared/external/closed-loop-delivery/SKILL.md +116 -0
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +492 -0
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +461 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +441 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +436 -0
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +527 -0
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +425 -0
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +409 -0
- package/pipeline/skills/shared/external/context-compression/SKILL.md +266 -0
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +491 -0
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +435 -0
- package/pipeline/skills/shared/external/core-motion/SKILL.md +388 -0
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +405 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +495 -0
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +420 -0
- package/pipeline/skills/shared/external/coreml/SKILL.md +458 -0
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +765 -0
- package/pipeline/skills/shared/external/css-modern/SKILL.md +467 -0
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +335 -0
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +422 -0
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +387 -0
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +298 -0
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +37 -0
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +477 -0
- package/pipeline/skills/shared/external/docker-expert/SKILL.md +413 -0
- package/pipeline/skills/shared/external/energykit/SKILL.md +460 -0
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +541 -0
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +483 -0
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +326 -0
- package/pipeline/skills/shared/external/fastapi-pro/SKILL.md +190 -0
- package/pipeline/skills/shared/external/firebase/SKILL.md +61 -0
- package/pipeline/skills/shared/external/github-actions-templates/SKILL.md +348 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +552 -0
- package/pipeline/skills/shared/external/healthkit/SKILL.md +498 -0
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +602 -0
- package/pipeline/skills/shared/external/help-skills/SKILL.md +166 -0
- package/pipeline/skills/shared/external/hig-components-content/SKILL.md +81 -0
- package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +95 -0
- package/pipeline/skills/shared/external/hig-components-status/SKILL.md +82 -0
- package/pipeline/skills/shared/external/hig-components-system/SKILL.md +101 -0
- package/pipeline/skills/shared/external/hig-foundations/SKILL.md +94 -0
- package/pipeline/skills/shared/external/hig-inputs/SKILL.md +110 -0
- package/pipeline/skills/shared/external/hig-patterns/SKILL.md +99 -0
- package/pipeline/skills/shared/external/hig-platforms/SKILL.md +81 -0
- package/pipeline/skills/shared/external/hig-technologies/SKILL.md +125 -0
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +496 -0
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +455 -0
- package/pipeline/skills/shared/external/html-semantic/SKILL.md +301 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +118 -0
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +301 -0
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +140 -0
- package/pipeline/skills/shared/external/ios-debugger-agent/SKILL.md +59 -0
- package/pipeline/skills/shared/external/ios-developer/SKILL.md +217 -0
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +418 -0
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +627 -0
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +462 -0
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +441 -0
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +862 -0
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -0
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +563 -0
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +1116 -0
- package/pipeline/skills/shared/external/ios-security/SKILL.md +496 -0
- package/pipeline/skills/shared/external/ios-security/references/app-review-guidelines.md +174 -0
- package/pipeline/skills/shared/external/ios-security/references/cryptokit-advanced.md +297 -0
- package/pipeline/skills/shared/external/ios-security/references/file-storage-patterns.md +354 -0
- package/pipeline/skills/shared/external/ios-security/references/privacy-manifest.md +117 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +101 -0
- package/pipeline/skills/shared/external/live-activities/SKILL.md +500 -0
- package/pipeline/skills/shared/external/live-activities/references/live-activity-patterns.md +868 -0
- package/pipeline/skills/shared/external/macos-menubar-tuist-app/SKILL.md +109 -0
- package/pipeline/skills/shared/external/macos-spm-app-packaging/SKILL.md +110 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +485 -0
- package/pipeline/skills/shared/external/mapkit-location/references/corelocation-patterns.md +730 -0
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +748 -0
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +479 -0
- package/pipeline/skills/shared/external/monorepo-architect/SKILL.md +64 -0
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +395 -0
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +363 -0
- package/pipeline/skills/shared/external/natural-language/SKILL.md +412 -0
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +311 -0
- package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +418 -0
- package/pipeline/skills/shared/external/nodejs-backend-patterns/SKILL.md +38 -0
- package/pipeline/skills/shared/external/observability-engineer/SKILL.md +235 -0
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +398 -0
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +254 -0
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +387 -0
- package/pipeline/skills/shared/external/pencilkit-drawing/references/paperkit-integration.md +376 -0
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +302 -0
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +446 -0
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +435 -0
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +501 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +701 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +774 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +869 -0
- package/pipeline/skills/shared/external/photos-camera-media/references/photospicker-patterns.md +597 -0
- package/pipeline/skills/shared/external/play-store-review/SKILL.md +350 -0
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +501 -0
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +677 -0
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +745 -0
- package/pipeline/skills/shared/external/python-patterns/SKILL.md +383 -0
- package/pipeline/skills/shared/external/react-best-practices/SKILL.md +290 -0
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +479 -0
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +480 -0
- package/pipeline/skills/shared/external/rest-api-design/SKILL.md +386 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +506 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +564 -0
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +483 -0
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +544 -0
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +485 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +478 -0
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +58 -0
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +755 -0
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +487 -0
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +895 -0
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +467 -0
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +408 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +80 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/swift-6-2-concurrency.md +233 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +187 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +341 -0
- package/pipeline/skills/shared/external/swift-concurrency-expert/SKILL.md +113 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/SKILL.md +124 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/actors.md +155 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/async-streams.md +67 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/bridging.md +52 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/bug-patterns.md +100 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/cancellation.md +107 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/diagnostics.md +70 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/hotspots.md +47 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/interop.md +129 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/new-features.md +224 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/structured.md +101 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/testing.md +218 -0
- package/pipeline/skills/shared/external/swift-concurrency-pro/references/unstructured.md +61 -0
- package/pipeline/skills/shared/external/swift-language/SKILL.md +498 -0
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +505 -0
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +462 -0
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +504 -0
- package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +97 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/async-tests.md +252 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/core-rules.md +52 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/migrating-from-xctest.md +34 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/new-features.md +318 -0
- package/pipeline/skills/shared/external/swift-testing-pro/references/writing-better-tests.md +254 -0
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +334 -0
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +504 -0
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +975 -0
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +675 -0
- package/pipeline/skills/shared/external/swiftdata-pro/SKILL.md +102 -0
- package/pipeline/skills/shared/external/swiftdata-pro/references/class-inheritance.md +104 -0
- package/pipeline/skills/shared/external/swiftdata-pro/references/cloudkit.md +10 -0
- package/pipeline/skills/shared/external/swiftdata-pro/references/core-rules.md +20 -0
- package/pipeline/skills/shared/external/swiftdata-pro/references/indexing.md +27 -0
- package/pipeline/skills/shared/external/swiftdata-pro/references/predicates.md +73 -0
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +503 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +821 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +553 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/SKILL.md +102 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/charts.md +602 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/latest-apis.md +464 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/liquid-glass.md +416 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/list-patterns.md +394 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-views.md +357 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/state-management.md +417 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/view-structure.md +389 -0
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +450 -0
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +425 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +336 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +97 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +69 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +99 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +147 -0
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +98 -0
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +262 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +207 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +177 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +169 -0
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +178 -0
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +371 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +486 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +1097 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +780 -0
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +696 -0
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +487 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +46 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +29 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +33 -0
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +52 -0
- package/pipeline/skills/shared/external/swiftui-performance-audit/SKILL.md +114 -0
- package/pipeline/skills/shared/external/swiftui-pro/SKILL.md +108 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/accessibility.md +13 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/api.md +39 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/data.md +43 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/design.md +31 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/hygiene.md +9 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/navigation.md +14 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/performance.md +46 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/swift.md +56 -0
- package/pipeline/skills/shared/external/swiftui-pro/references/views.md +35 -0
- package/pipeline/skills/shared/external/swiftui-ui-patterns/SKILL.md +103 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +428 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +534 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +948 -0
- package/pipeline/skills/shared/external/swiftui-view-refactor/SKILL.md +210 -0
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +273 -0
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +151 -0
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +95 -0
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +51 -0
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +111 -0
- package/pipeline/skills/shared/external/tailwind-css/SKILL.md +309 -0
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +393 -0
- package/pipeline/skills/shared/external/tipkit/SKILL.md +494 -0
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +782 -0
- package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +336 -0
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +475 -0
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +736 -0
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +738 -0
- package/pipeline/skills/shared/external/vue-composition/SKILL.md +371 -0
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +410 -0
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +567 -0
- package/pipeline/skills/shared/external/web-accessibility/SKILL.md +373 -0
- package/pipeline/skills/shared/external/web-performance/SKILL.md +345 -0
- package/pipeline/skills/shared/external/web-testing/SKILL.md +385 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +497 -0
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +871 -0
- 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())
|