@1aboveio/skills 0.0.0-stage → 1.0.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/README.md +30 -2
- package/bin/cli.mjs +14 -0
- package/bin/select-core.mjs +172 -0
- package/package.json +146 -4
- package/skills/app-dev/airflow/LICENSE +3 -0
- package/skills/app-dev/airflow/SKILL.md +199 -0
- package/skills/app-dev/airflow/release-check.mjs +93 -0
- package/skills/app-dev/android-foundation/LICENSE +3 -0
- package/skills/app-dev/android-foundation/SKILL.md +192 -0
- package/skills/app-dev/android-foundation/assets/android-ci-job.yml +217 -0
- package/skills/app-dev/android-foundation/references/ci.md +87 -0
- package/skills/app-dev/android-foundation/references/gotchas.md +221 -0
- package/skills/app-dev/android-foundation/references/setup.md +164 -0
- package/skills/app-dev/android-foundation/release-check.mjs +93 -0
- package/skills/app-dev/android-foundation/scripts/device-run.sh +176 -0
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +172 -0
- package/skills/app-dev/app-debug/LICENSE +3 -0
- package/skills/app-dev/app-debug/SKILL.md +110 -0
- package/skills/app-dev/app-debug/references/common-errors.md +128 -0
- package/skills/app-dev/app-debug/release-check.mjs +93 -0
- package/skills/app-dev/better-auth/LICENSE +3 -0
- package/skills/app-dev/better-auth/SKILL.md +598 -0
- package/skills/app-dev/better-auth/references/feishu-api.md +270 -0
- package/skills/app-dev/better-auth/release-check.mjs +93 -0
- package/skills/app-dev/data-catalog/LICENSE +3 -0
- package/skills/app-dev/data-catalog/SKILL.md +204 -0
- package/skills/app-dev/data-catalog/agents/openai.yaml +3 -0
- package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +75 -0
- package/skills/app-dev/data-catalog/references/field-definitions.md +116 -0
- package/skills/app-dev/data-catalog/references/format.md +121 -0
- package/skills/app-dev/data-catalog/release-check.mjs +93 -0
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +36 -0
- package/skills/app-dev/data-catalog/scripts/data_catalog.py +1392 -0
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +117 -0
- package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +17 -0
- package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +571 -0
- package/skills/app-dev/ios-foundation/LICENSE +3 -0
- package/skills/app-dev/ios-foundation/SKILL.md +184 -0
- package/skills/app-dev/ios-foundation/references/setup.md +243 -0
- package/skills/app-dev/ios-foundation/release-check.mjs +93 -0
- package/skills/app-dev/monorepo/LICENSE +3 -0
- package/skills/app-dev/monorepo/SKILL.md +462 -0
- package/skills/app-dev/monorepo/release-check.mjs +93 -0
- package/skills/app-dev/nextjs-fullstack/LICENSE +3 -0
- package/skills/app-dev/nextjs-fullstack/SKILL.md +211 -0
- package/skills/app-dev/nextjs-fullstack/conventions.md +318 -0
- package/skills/app-dev/nextjs-fullstack/frontend-conventions.md +61 -0
- package/skills/app-dev/nextjs-fullstack/nextjs16.md +287 -0
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +93 -0
- package/skills/app-dev/nextjs-fullstack/server-actions.md +409 -0
- package/skills/app-dev/prisma-setup/LICENSE +3 -0
- package/skills/app-dev/prisma-setup/SKILL.md +181 -0
- package/skills/app-dev/prisma-setup/nextjs.md +258 -0
- package/skills/app-dev/prisma-setup/release-check.mjs +93 -0
- package/skills/app-dev/prisma-setup/turborepo.md +301 -0
- package/skills/app-dev/pyspark/LICENSE +3 -0
- package/skills/app-dev/pyspark/SKILL.md +127 -0
- package/skills/app-dev/pyspark/assets/templates/etl.py +748 -0
- package/skills/app-dev/pyspark/assets/templates/utils/__init__.py +1 -0
- package/skills/app-dev/pyspark/assets/templates/utils/hudi_metadata.py +14 -0
- package/skills/app-dev/pyspark/references/diagnosis-and-profiling.md +169 -0
- package/skills/app-dev/pyspark/references/etl-contract.md +129 -0
- package/skills/app-dev/pyspark/references/production-validation.md +131 -0
- package/skills/app-dev/pyspark/references/reconciliation.md +38 -0
- package/skills/app-dev/pyspark/references/transformation-design.md +190 -0
- package/skills/app-dev/pyspark/references/velocity-feature-calculation.md +193 -0
- package/skills/app-dev/pyspark/release-check.mjs +93 -0
- package/skills/app-dev/pyspark/scripts/spark_eventlog_summary.py +223 -0
- package/skills/app-dev/python-backend/LICENSE +3 -0
- package/skills/app-dev/python-backend/SKILL.md +325 -0
- package/skills/app-dev/python-backend/assets/uv-install.sh +48 -0
- package/skills/app-dev/python-backend/references/uv-installs.md +83 -0
- package/skills/app-dev/python-backend/release-check.mjs +93 -0
- package/skills/app-dev/shadcn/LICENSE +3 -0
- package/skills/app-dev/shadcn/SKILL.md +120 -0
- package/skills/app-dev/shadcn/assets/shadcn-small.png +0 -0
- package/skills/app-dev/shadcn/assets/shadcn.png +0 -0
- package/skills/app-dev/shadcn/cli.md +411 -0
- package/skills/app-dev/shadcn/customization.md +224 -0
- package/skills/app-dev/shadcn/mcp.md +101 -0
- package/skills/app-dev/shadcn/release-check.mjs +93 -0
- package/skills/app-dev/shadcn/rules/base-vs-radix.md +323 -0
- package/skills/app-dev/shadcn/rules/component-selection.md +67 -0
- package/skills/app-dev/shadcn/rules/composition.md +195 -0
- package/skills/app-dev/shadcn/rules/data-table.md +201 -0
- package/skills/app-dev/shadcn/rules/forms.md +255 -0
- package/skills/app-dev/shadcn/rules/icons.md +103 -0
- package/skills/app-dev/shadcn/rules/styling.md +167 -0
- package/skills/app-dev/zod-v4/LICENSE +3 -0
- package/skills/app-dev/zod-v4/SKILL.md +286 -0
- package/skills/app-dev/zod-v4/release-check.mjs +93 -0
- package/skills/cicd/cloud-build/LICENSE +3 -0
- package/skills/cicd/cloud-build/SKILL.md +707 -0
- package/skills/cicd/cloud-build/release-check.mjs +93 -0
- package/skills/cicd/cloud-debug/LICENSE +3 -0
- package/skills/cicd/cloud-debug/SKILL.md +315 -0
- package/skills/cicd/cloud-debug/references/build-failures.md +238 -0
- package/skills/cicd/cloud-debug/references/deploy-failures.md +376 -0
- package/skills/cicd/cloud-debug/references/pipeline-failures.md +378 -0
- package/skills/cicd/cloud-debug/release-check.mjs +93 -0
- package/skills/cicd/cloud-deploy/LICENSE +3 -0
- package/skills/cicd/cloud-deploy/SKILL.md +230 -0
- package/skills/cicd/cloud-deploy/references/config-templates.md +257 -0
- package/skills/cicd/cloud-deploy/release-check.mjs +93 -0
- package/skills/cicd/docker/LICENSE +3 -0
- package/skills/cicd/docker/SKILL.md +151 -0
- package/skills/cicd/docker/release-check.mjs +93 -0
- package/skills/cicd/github-actions/LICENSE +3 -0
- package/skills/cicd/github-actions/SKILL.md +111 -0
- package/skills/cicd/github-actions/references/audit-profile-optimize.md +150 -0
- package/skills/cicd/github-actions/references/ci-case-studies.md +141 -0
- package/skills/cicd/github-actions/references/conditional-ci.md +214 -0
- package/skills/cicd/github-actions/references/debugging.md +13 -0
- package/skills/cicd/github-actions/references/pipeline.md +87 -0
- package/skills/cicd/github-actions/references/post-deploy-and-publishing.md +33 -0
- package/skills/cicd/github-actions/references/runners.md +253 -0
- package/skills/cicd/github-actions/references/security.md +29 -0
- package/skills/cicd/github-actions/references/speed-and-audit.md +115 -0
- package/skills/cicd/github-actions/references/two-pass-reading.md +92 -0
- package/skills/cicd/github-actions/release-check.mjs +93 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit-core.mjs +612 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit.mjs +4 -0
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +637 -0
- package/skills/cicd/github-flow/LICENSE +3 -0
- package/skills/cicd/github-flow/SKILL.md +123 -0
- package/skills/cicd/github-flow/references/ci-conventions.md +14 -0
- package/skills/cicd/github-flow/release-check.mjs +93 -0
- package/skills/cicd/github-flow/scripts/flow-core.mjs +105 -0
- package/skills/cicd/github-flow/scripts/flow.mjs +81 -0
- package/skills/cicd/github-flow/scripts/flow.test.mjs +114 -0
- package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +117 -0
- package/skills/cicd/github-flow/scripts/persist-conventions.mjs +49 -0
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +108 -0
- package/skills/cicd/google-cloud/LICENSE +3 -0
- package/skills/cicd/google-cloud/SKILL.md +119 -0
- package/skills/cicd/google-cloud/references/gcs.md +469 -0
- package/skills/cicd/google-cloud/references/iam.md +451 -0
- package/skills/cicd/google-cloud/references/project.md +349 -0
- package/skills/cicd/google-cloud/references/secrets.md +336 -0
- package/skills/cicd/google-cloud/references/vpc.md +312 -0
- package/skills/cicd/google-cloud/release-check.mjs +93 -0
- package/skills/cicd/google-cloud/scripts/create-sa.sh +36 -0
- package/skills/cicd/google-cloud/scripts/gcp-config.sh +31 -0
- package/skills/cicd/google-cloud/scripts/grant-iap.sh +41 -0
- package/skills/cicd/google-cloud/scripts/setup-secrets.sh +48 -0
- package/skills/cicd/mergify/LICENSE +3 -0
- package/skills/cicd/mergify/SKILL.md +102 -0
- package/skills/cicd/mergify/assets/templates/mergify.yml +215 -0
- package/skills/cicd/mergify/assets/templates/ruleset.json +46 -0
- package/skills/cicd/mergify/references/branch-protection.md +282 -0
- package/skills/cicd/mergify/references/configuration.md +274 -0
- package/skills/cicd/mergify/references/diagnosis.md +109 -0
- package/skills/cicd/mergify/references/stacks.md +84 -0
- package/skills/cicd/mergify/references/traps.md +119 -0
- package/skills/cicd/mergify/references/watch-contract.md +274 -0
- package/skills/cicd/mergify/release-check.mjs +93 -0
- package/skills/cicd/mergify/scripts/audit-core.mjs +208 -0
- package/skills/cicd/mergify/scripts/audit.mjs +4 -0
- package/skills/cicd/mergify/scripts/audit.test.mjs +201 -0
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +106 -0
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +30 -0
- package/skills/cicd/mergify/scripts/stack-push-core.mjs +258 -0
- package/skills/cicd/mergify/scripts/stack-push.mjs +4 -0
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +233 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery-core.mjs +1065 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery.mjs +4 -0
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +1826 -0
- package/skills/cicd/podman/LICENSE +3 -0
- package/skills/cicd/podman/SKILL.md +71 -0
- package/skills/cicd/podman/agents/openai.yaml +4 -0
- package/skills/cicd/podman/assets/templates/podman-compose-socket-directory.yml +6 -0
- package/skills/cicd/podman/assets/templates/podman-service-override.conf +3 -0
- package/skills/cicd/podman/references/compose-compatibility.md +70 -0
- package/skills/cicd/podman/references/networking-and-ports.md +74 -0
- package/skills/cicd/podman/references/rootless-services-and-sockets.md +156 -0
- package/skills/cicd/podman/references/troubleshooting.md +98 -0
- package/skills/cicd/podman/release-check.mjs +93 -0
- package/skills/content/article-review/LICENSE +3 -0
- package/skills/content/article-review/SKILL.md +66 -0
- package/skills/content/article-review/references/report-format.md +68 -0
- package/skills/content/article-review/references/template-guide.md +79 -0
- package/skills/content/article-review/references/template-wechat-payments.md +125 -0
- package/skills/content/article-review/release-check.mjs +93 -0
- package/skills/content/article-to-image/LICENSE +3 -0
- package/skills/content/article-to-image/SKILL.md +91 -0
- package/skills/content/article-to-image/references/formats.md +53 -0
- package/skills/content/article-to-image/release-check.mjs +93 -0
- package/skills/content/flowchart-svg/LICENSE +3 -0
- package/skills/content/flowchart-svg/SKILL.md +43 -0
- package/skills/content/flowchart-svg/release-check.mjs +93 -0
- package/skills/content/flowchart-svg/scripts/export_png.sh +8 -0
- package/skills/content/image-gen/LICENSE +3 -0
- package/skills/content/image-gen/SKILL.md +192 -0
- package/skills/content/image-gen/release-check.mjs +93 -0
- package/skills/content/image-gen/scripts/credentials.mjs +217 -0
- package/skills/content/image-gen/scripts/credentials.test.mjs +212 -0
- package/skills/content/image-gen/scripts/generate.mjs +240 -0
- package/skills/content/image-gen/scripts/options.mjs +293 -0
- package/skills/content/image-gen/scripts/options.test.mjs +245 -0
- package/skills/content/mermaid/LICENSE +3 -0
- package/skills/content/mermaid/SKILL.md +635 -0
- package/skills/content/mermaid/release-check.mjs +93 -0
- package/skills/content/wechat-article-writer/LICENSE +3 -0
- package/skills/content/wechat-article-writer/SKILL.md +92 -0
- package/skills/content/wechat-article-writer/references/style-selector.md +133 -0
- package/skills/content/wechat-article-writer/references/writing-rules.md +152 -0
- package/skills/content/wechat-article-writer/release-check.mjs +93 -0
- package/skills/content/wechat-article-writer/templates/wechat-comparison.md +52 -0
- package/skills/content/wechat-article-writer/templates/wechat-howto.md +51 -0
- package/skills/content/wechat-article-writer/templates/wechat-insight.md +50 -0
- package/skills/content/wechat-article-writer/templates/wechat-pitfall.md +52 -0
- package/skills/content/wechat-publisher/LICENSE +3 -0
- package/skills/content/wechat-publisher/SKILL.md +171 -0
- package/skills/content/wechat-publisher/references/api.md +105 -0
- package/skills/content/wechat-publisher/references/layout-rules.md +57 -0
- package/skills/content/wechat-publisher/references/wechat-safe-template.html +86 -0
- package/skills/content/wechat-publisher/release-check.mjs +93 -0
- package/skills/content/wechat-publisher/scripts/md_to_wechat_html.py +194 -0
- package/skills/content/wechat-publisher/scripts/publish.py +239 -0
- package/skills/content/wechat-search/LICENSE +3 -0
- package/skills/content/wechat-search/SKILL.md +211 -0
- package/skills/content/wechat-search/release-check.mjs +93 -0
- package/skills/content/wechat-search/scripts/start-chrome.sh +83 -0
- package/skills/content/xai-search/LICENSE +3 -0
- package/skills/content/xai-search/SKILL.md +79 -0
- package/skills/content/xai-search/release-check.mjs +93 -0
- package/skills/content/xai-search/scripts/xai_search.sh +187 -0
- package/skills/content/xai-search/tests/smoke_test.sh +36 -0
- package/skills/core/distill-writing/LICENSE +3 -0
- package/skills/core/distill-writing/SKILL.md +74 -0
- package/skills/core/distill-writing/references/extraction.md +95 -0
- package/skills/core/distill-writing/references/updating.md +46 -0
- package/skills/core/distill-writing/release-check.mjs +93 -0
- package/skills/core/doc-to-md/LICENSE +3 -0
- package/skills/core/doc-to-md/SKILL.md +78 -0
- package/skills/core/doc-to-md/release-check.mjs +93 -0
- package/skills/core/doc-to-md/scripts/convert.py +157 -0
- package/skills/core/editorial-review/LICENSE +3 -0
- package/skills/core/editorial-review/SKILL.md +79 -0
- package/skills/core/editorial-review/references/checklist.md +94 -0
- package/skills/core/editorial-review/release-check.mjs +93 -0
- package/skills/core/house-style/LICENSE +3 -0
- package/skills/core/house-style/SKILL.md +33 -0
- package/skills/core/house-style/references/defaults.md +66 -0
- package/skills/core/house-style/references/rules.md +267 -0
- package/skills/core/house-style/references/sources.md +27 -0
- package/skills/core/house-style/references/style-en.md +131 -0
- package/skills/core/house-style/references/style-zh.md +141 -0
- package/skills/core/house-style/references/terminology-zh.md +223 -0
- package/skills/core/house-style/release-check.mjs +93 -0
- package/skills/core/house-style/templates/brd.md +103 -0
- package/skills/core/house-style/templates/commercial-proposal.md +93 -0
- package/skills/core/house-style/templates/compliance-policy.md +109 -0
- package/skills/core/house-style/templates/gap-analysis.md +105 -0
- package/skills/core/house-style/templates/general.md +63 -0
- package/skills/core/house-style/templates/integration-guide.md +98 -0
- package/skills/core/house-style/templates/prd.md +81 -0
- package/skills/core/house-style/templates/risk-operations-procedure.md +94 -0
- package/skills/core/house-style/templates/technical-spec.md +96 -0
- package/skills/core/polish-cn/LICENSE +3 -0
- package/skills/core/polish-cn/SKILL.md +102 -0
- package/skills/core/polish-cn/references/brief.md +93 -0
- package/skills/core/polish-cn/references/models.md +76 -0
- package/skills/core/polish-cn/release-check.mjs +93 -0
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +244 -0
- package/skills/core/polish-cn/scripts/polish_blocks.py +489 -0
- package/skills/core/reg-parser/LICENSE +3 -0
- package/skills/core/reg-parser/SKILL.md +212 -0
- package/skills/core/reg-parser/references/checklist-schema.md +223 -0
- package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.json +1306 -0
- package/skills/core/reg-parser/references/checklists/hk-amlo-checklist.md +469 -0
- package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.json +634 -0
- package/skills/core/reg-parser/references/checklists/hk-fi-corporate-onboarding-cdd-checklist.md +233 -0
- package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.json +2194 -0
- package/skills/core/reg-parser/references/checklists/hk-mso-guideline-checklist.md +771 -0
- package/skills/core/reg-parser/references/checklists/kyc-aml-checklist.md +258 -0
- package/skills/core/reg-parser/references/checklists/visa-vars-checklist.md +1058 -0
- package/skills/core/reg-parser/references/mandates/hk-amlo-cap615.md +389 -0
- package/skills/core/reg-parser/references/mandates/visa-vars.md +205 -0
- package/skills/core/reg-parser/release-check.mjs +93 -0
- package/skills/core/reg-parser/scripts/compare_checklist.py +230 -0
- package/skills/core/reg-parser/scripts/dump_checklist.py +361 -0
- package/skills/core/reg-parser/scripts/preprocess-mandate.sh +187 -0
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +165 -0
- package/skills/core/reg-parser/scripts/test_quarantine.sh +105 -0
- package/skills/core/reg-parser/scripts/validate_checklist.py +563 -0
- package/skills/core/reg-parser/scripts/validate_mandate.py +273 -0
- package/skills/core/terminology/LICENSE +3 -0
- package/skills/core/terminology/SKILL.md +62 -0
- package/skills/core/terminology/references/format.md +50 -0
- package/skills/core/terminology/references/init.md +32 -0
- package/skills/core/terminology/release-check.mjs +93 -0
- package/skills/core/write-doc/LICENSE +3 -0
- package/skills/core/write-doc/SKILL.md +141 -0
- package/skills/core/write-doc/references/bilingual.md +60 -0
- package/skills/core/write-doc/release-check.mjs +93 -0
- package/skills/engineering/better-review/LICENSE +3 -0
- package/skills/engineering/better-review/SKILL.md +249 -0
- package/skills/engineering/better-review/agents/openai.yaml +9 -0
- package/skills/engineering/better-review/references/domain-hazards.md +53 -0
- package/skills/engineering/better-review/references/passes.md +147 -0
- package/skills/engineering/better-review/references/result.md +151 -0
- package/skills/engineering/better-review/release-check.mjs +93 -0
- package/skills/engineering/better-review/scripts/pr-context.mjs +212 -0
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +168 -0
- package/skills/engineering/better-review/scripts/rereview-scope.mjs +503 -0
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +587 -0
- package/skills/engineering/better-review/scripts/scan-diff.mjs +365 -0
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +414 -0
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +96 -0
- package/skills/engineering/e2e-test/LICENSE +3 -0
- package/skills/engineering/e2e-test/SKILL.md +98 -0
- package/skills/engineering/e2e-test/assets/ci-gates.cloudbuild.yaml +24 -0
- package/skills/engineering/e2e-test/references/authoring/auth-flows.md +159 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-config.md +73 -0
- package/skills/engineering/e2e-test/references/authoring/playwright-patterns.md +144 -0
- package/skills/engineering/e2e-test/references/ci-integration.md +25 -0
- package/skills/engineering/e2e-test/references/ci-playwright-container.md +290 -0
- package/skills/engineering/e2e-test/references/presentation-sweep.md +104 -0
- package/skills/engineering/e2e-test/references/reviewing.md +41 -0
- package/skills/engineering/e2e-test/references/route-manifest.md +51 -0
- package/skills/engineering/e2e-test/release-check.mjs +93 -0
- package/skills/engineering/e2e-test/scripts/detect-routes-nextjs.mjs +200 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.mjs +171 -0
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +106 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.mjs +179 -0
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +117 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.mjs +154 -0
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +69 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.mjs +141 -0
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +98 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.mjs +147 -0
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +71 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.mjs +127 -0
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +59 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.mjs +82 -0
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +76 -0
- package/skills/engineering/e2e-test/scripts/validate-manifest.mjs +106 -0
- package/skills/engineering/engineering-runtime/LICENSE +3 -0
- package/skills/engineering/engineering-runtime/SKILL.md +11 -0
- package/skills/engineering/engineering-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +597 -0
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +68 -0
- package/skills/engineering/engineering-runtime/release-check.mjs +93 -0
- package/skills/engineering/engineering-runtime/scripts/exact-head-artifact.mjs +131 -0
- package/skills/engineering/engineering-runtime/scripts/head-check-set.mjs +398 -0
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.mjs +187 -0
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +95 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.mjs +79 -0
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +288 -0
- package/skills/engineering/engineering-runtime/scripts/mergify-yaml.mjs +11 -0
- package/skills/engineering/engineering-runtime/scripts/package-lock.json +43 -0
- package/skills/engineering/engineering-runtime/scripts/package.json +10 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.mjs +223 -0
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +96 -0
- package/skills/engineering/engineering-runtime/scripts/review-completion.mjs +56 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +595 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +189 -0
- package/skills/engineering/engineering-runtime/scripts/yaml-mapping.mjs +18 -0
- package/skills/engineering/ensure-coverage/LICENSE +3 -0
- package/skills/engineering/ensure-coverage/SKILL.md +129 -0
- package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +92 -0
- package/skills/engineering/ensure-coverage/references/breadth/inventory-contract.md +85 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +44 -0
- package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +16 -0
- package/skills/engineering/ensure-coverage/references/depth/characterization.md +68 -0
- package/skills/engineering/ensure-coverage/references/depth/coverage.config.example.json +19 -0
- package/skills/engineering/ensure-coverage/references/depth/grading.md +35 -0
- package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +57 -0
- package/skills/engineering/ensure-coverage/references/depth/test-smells.md +39 -0
- package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +44 -0
- package/skills/engineering/ensure-coverage/references/examples/coverage-ledger.md +110 -0
- package/skills/engineering/ensure-coverage/references/examples/refund-flow.md +35 -0
- package/skills/engineering/ensure-coverage/references/presentation/axis.md +64 -0
- package/skills/engineering/ensure-coverage/references/process/audit-mode.md +27 -0
- package/skills/engineering/ensure-coverage/references/process/output-template.md +139 -0
- package/skills/engineering/ensure-coverage/references/process/test-strategy.md +56 -0
- package/skills/engineering/ensure-coverage/release-check.mjs +93 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-config.schema.json +67 -0
- package/skills/engineering/ensure-coverage/schemas/coverage-file.schema.json +140 -0
- package/skills/engineering/ensure-coverage/scripts/README.md +99 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.mjs +182 -0
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +96 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.mjs +105 -0
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +47 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.mjs +137 -0
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +86 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.mjs +674 -0
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +447 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.mjs +591 -0
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +552 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.mjs +282 -0
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +289 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +119 -0
- package/skills/engineering/ensure-coverage/scripts/package.json +19 -0
- package/skills/engineering/golden-path/LICENSE +3 -0
- package/skills/engineering/golden-path/SKILL.md +230 -0
- package/skills/engineering/golden-path/agents/openai.yaml +3 -0
- package/skills/engineering/golden-path/assets/golden-paths.example.json +57 -0
- package/skills/engineering/golden-path/references/contract.md +239 -0
- package/skills/engineering/golden-path/references/migration.md +111 -0
- package/skills/engineering/golden-path/release-check.mjs +93 -0
- package/skills/engineering/golden-path/scripts/golden-path-core.mjs +1779 -0
- package/skills/engineering/golden-path/scripts/golden-path.mjs +6 -0
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +1457 -0
- package/skills/engineering/harness-runtime/LICENSE +3 -0
- package/skills/engineering/harness-runtime/SKILL.md +19 -0
- package/skills/engineering/harness-runtime/agents/openai.yaml +6 -0
- package/skills/engineering/harness-runtime/bin/discover-models.mjs +4 -0
- package/skills/engineering/harness-runtime/bin/model-catalog.mjs +4 -0
- package/skills/engineering/harness-runtime/contracts.md +15 -0
- package/skills/engineering/harness-runtime/discover-models.mjs +392 -0
- package/skills/engineering/harness-runtime/discover-models.test.mjs +363 -0
- package/skills/engineering/harness-runtime/fixtures/native-question-schemas.json +33 -0
- package/skills/engineering/harness-runtime/fixtures/question-responses.json +54 -0
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +967 -0
- package/skills/engineering/harness-runtime/index.mjs +767 -0
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +829 -0
- package/skills/engineering/harness-runtime/model-catalog.mjs +787 -0
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +366 -0
- package/skills/engineering/harness-runtime/native-question-contracts.md +37 -0
- package/skills/engineering/harness-runtime/references/model-catalog-seed.json +408 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +57 -0
- package/skills/engineering/harness-runtime/release-check.mjs +93 -0
- package/skills/engineering/resolve-issues/LICENSE +3 -0
- package/skills/engineering/resolve-issues/SKILL.md +250 -0
- package/skills/engineering/resolve-issues/agents/openai.yaml +9 -0
- package/skills/engineering/resolve-issues/plugin-hooks.json +17 -0
- package/skills/engineering/resolve-issues/references/canary.md +28 -0
- package/skills/engineering/resolve-issues/references/ci.md +105 -0
- package/skills/engineering/resolve-issues/references/done.md +25 -0
- package/skills/engineering/resolve-issues/references/dropbox.md +90 -0
- package/skills/engineering/resolve-issues/references/expire.md +318 -0
- package/skills/engineering/resolve-issues/references/exploration.md +202 -0
- package/skills/engineering/resolve-issues/references/implementation.md +303 -0
- package/skills/engineering/resolve-issues/references/preflight.md +445 -0
- package/skills/engineering/resolve-issues/references/review.md +228 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +44 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +264 -0
- package/skills/engineering/resolve-issues/release-check.mjs +93 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +364 -0
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +280 -0
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/dropbox.mjs +473 -0
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +346 -0
- package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +98 -0
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +88 -0
- package/skills/engineering/resolve-issues/scripts/github-reviews.mjs +384 -0
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +264 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +135 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +79 -0
- package/skills/engineering/resolve-issues/scripts/issue-tree.mjs +162 -0
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +107 -0
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +14 -0
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +9 -0
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +149 -0
- package/skills/engineering/resolve-issues/scripts/next-actions.mjs +856 -0
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +915 -0
- package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +269 -0
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +156 -0
- package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +736 -0
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +448 -0
- package/skills/engineering/resolve-issues/scripts/ready-check.mjs +185 -0
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +217 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +98 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +91 -0
- package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +509 -0
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +1060 -0
- package/skills/engineering/resolve-issues/scripts/size-check.mjs +492 -0
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +458 -0
- package/skills/engineering/resolve-release/LICENSE +3 -0
- package/skills/engineering/resolve-release/SKILL.md +215 -0
- package/skills/engineering/resolve-release/agents/openai.yaml +8 -0
- package/skills/engineering/resolve-release/flows/cloud-run-canary.md +13 -0
- package/skills/engineering/resolve-release/flows/cloud-run.md +7 -0
- package/skills/engineering/resolve-release/flows/root-tag-npm.md +13 -0
- package/skills/engineering/resolve-release/flows/workspace-npm.md +22 -0
- package/skills/engineering/resolve-release/references/canary.md +51 -0
- package/skills/engineering/resolve-release/references/candidate.md +41 -0
- package/skills/engineering/resolve-release/references/cut.md +131 -0
- package/skills/engineering/resolve-release/references/dark-launch.md +66 -0
- package/skills/engineering/resolve-release/references/flows.md +150 -0
- package/skills/engineering/resolve-release/references/preflight.md +154 -0
- package/skills/engineering/resolve-release/references/prepared-metadata.md +260 -0
- package/skills/engineering/resolve-release/references/promotion.md +82 -0
- package/skills/engineering/resolve-release/references/publish.md +232 -0
- package/skills/engineering/resolve-release/references/workspace-npm.md +103 -0
- package/skills/engineering/resolve-release/release-check.mjs +93 -0
- package/skills/engineering/resolve-release/scripts/apply.mjs +212 -0
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +99 -0
- package/skills/engineering/resolve-release/scripts/dark-flags.mjs +134 -0
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +100 -0
- package/skills/engineering/resolve-release/scripts/flow.mjs +500 -0
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +340 -0
- package/skills/engineering/resolve-release/scripts/green-head.mjs +231 -0
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +101 -0
- package/skills/engineering/resolve-release/scripts/npm-version-absence.mjs +138 -0
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +426 -0
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +659 -0
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +317 -0
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +291 -0
- package/skills/engineering/resolve-release/scripts/plan.mjs +806 -0
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +428 -0
- package/skills/engineering/rush-issues/LICENSE +3 -0
- package/skills/engineering/rush-issues/SKILL.md +58 -0
- package/skills/engineering/rush-issues/agents/openai.yaml +8 -0
- package/skills/engineering/rush-issues/rehydrate.mjs +10 -0
- package/skills/engineering/rush-issues/release-check.mjs +93 -0
- package/skills/engineering/rush-release/LICENSE +3 -0
- package/skills/engineering/rush-release/SKILL.md +35 -0
- package/skills/engineering/rush-release/agents/openai.yaml +8 -0
- package/skills/engineering/rush-release/release-check.mjs +93 -0
- package/skills/engineering/test-quality/LICENSE +3 -0
- package/skills/engineering/test-quality/SKILL.md +79 -0
- package/skills/engineering/test-quality/references/audit.md +80 -0
- package/skills/engineering/test-quality/references/lint.md +24 -0
- package/skills/engineering/test-quality/references/principles.md +143 -0
- package/skills/engineering/test-quality/references/testing-conventions.md +9 -0
- package/skills/engineering/test-quality/release-check.mjs +93 -0
- package/skills/engineering/test-quality/scripts/lint-tests.mjs +574 -0
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +591 -0
- package/skills/engineering/test-quality/scripts/mock-policy-config.mjs +176 -0
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +119 -0
- package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +117 -0
- package/skills/engineering/test-quality/scripts/persist-conventions.mjs +17 -0
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +104 -0
- package/skills/risk-compliance/compliance-audit/LICENSE +3 -0
- package/skills/risk-compliance/compliance-audit/SKILL.md +439 -0
- package/skills/risk-compliance/compliance-audit/references/findings-schema.md +293 -0
- package/skills/risk-compliance/compliance-audit/references/normalization-rules.md +45 -0
- package/skills/risk-compliance/compliance-audit/references/vars-checklist.md +1068 -0
- package/skills/risk-compliance/compliance-audit/references/vars-findings-schema.md +398 -0
- package/skills/risk-compliance/compliance-audit/release-check.mjs +93 -0
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +14 -0
- package/skills/risk-compliance/compliance-audit/scripts/export_tracker_xlsx.py +221 -0
- package/skills/risk-compliance/compliance-audit/scripts/reconcile_tracker.py +458 -0
- package/skills/risk-compliance/compliance-audit/scripts/test_export_tracker_xlsx.py +97 -0
- package/skills/risk-compliance/compliance-audit/scripts/test_validate_tracker.py +157 -0
- package/skills/risk-compliance/compliance-audit/scripts/validate_tracker.py +829 -0
- package/skills/risk-compliance/contract-compare/LICENSE +3 -0
- package/skills/risk-compliance/contract-compare/SKILL.md +199 -0
- package/skills/risk-compliance/contract-compare/references/comparison-dimensions.md +209 -0
- package/skills/risk-compliance/contract-compare/release-check.mjs +93 -0
- package/skills/risk-compliance/contract-review/LICENSE +3 -0
- package/skills/risk-compliance/contract-review/SKILL.md +240 -0
- package/skills/risk-compliance/contract-review/references/review-criteria.md +223 -0
- package/skills/risk-compliance/contract-review/release-check.mjs +93 -0
- package/skills/risk-compliance/doc-authenticity/LICENSE +3 -0
- package/skills/risk-compliance/doc-authenticity/SKILL.md +160 -0
- package/skills/risk-compliance/doc-authenticity/agents/openai.yaml +8 -0
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +12 -0
- package/skills/risk-compliance/doc-authenticity/references/agreement.md +66 -0
- package/skills/risk-compliance/doc-authenticity/references/bank-statement.md +150 -0
- package/skills/risk-compliance/doc-authenticity/references/forensic-signals.md +157 -0
- package/skills/risk-compliance/doc-authenticity/references/image-signals.md +114 -0
- package/skills/risk-compliance/doc-authenticity/references/invoice.md +66 -0
- package/skills/risk-compliance/doc-authenticity/references/issuer-profiles.md +86 -0
- package/skills/risk-compliance/doc-authenticity/references/verdict-and-report.md +95 -0
- package/skills/risk-compliance/doc-authenticity/references/verdict.schema.json +81 -0
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +93 -0
- package/skills/risk-compliance/doc-authenticity/scripts/image_forensics.py +294 -0
- package/skills/risk-compliance/doc-authenticity/scripts/pdf_forensics.py +401 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_codes.py +224 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.py +308 -0
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +212 -0
- package/skills/risk-compliance/fraud-analysis/LICENSE +3 -0
- package/skills/risk-compliance/fraud-analysis/SKILL.md +114 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/authorized-payment-scam.md +41 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/first-party-fraud.md +44 -0
- package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +27 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/bank-transfer.md +24 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +30 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +20 -0
- package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +20 -0
- package/skills/risk-compliance/fraud-analysis/references/feature-engineering.md +158 -0
- package/skills/risk-compliance/fraud-analysis/references/mechanisms/account-takeover.md +36 -0
- package/skills/risk-compliance/fraud-analysis/references/report-rationale.md +45 -0
- package/skills/risk-compliance/fraud-analysis/references/report-template.md +190 -0
- package/skills/risk-compliance/fraud-analysis/references/review-checklist.md +175 -0
- package/skills/risk-compliance/fraud-analysis/references/taxonomy.md +79 -0
- package/skills/risk-compliance/fraud-analysis/references/terminology.md +108 -0
- package/skills/risk-compliance/fraud-analysis/references/workflow.md +175 -0
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +93 -0
- package/skills/risk-compliance/payment-analysis/LICENSE +3 -0
- package/skills/risk-compliance/payment-analysis/SKILL.md +180 -0
- package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +30 -0
- package/skills/risk-compliance/payment-analysis/references/chargebacks.md +208 -0
- package/skills/risk-compliance/payment-analysis/references/chinese-polish.md +51 -0
- package/skills/risk-compliance/payment-analysis/references/event-layers.md +127 -0
- package/skills/risk-compliance/payment-analysis/references/fx.md +59 -0
- package/skills/risk-compliance/payment-analysis/references/journey.md +79 -0
- package/skills/risk-compliance/payment-analysis/references/metrics.md +67 -0
- package/skills/risk-compliance/payment-analysis/references/report-template.md +119 -0
- package/skills/risk-compliance/payment-analysis/references/terminology.md +95 -0
- package/skills/risk-compliance/payment-analysis/references/visualization.md +56 -0
- package/skills/risk-compliance/payment-analysis/release-check.mjs +93 -0
- package/skills/risk-compliance/report-writing/LICENSE +3 -0
- package/skills/risk-compliance/report-writing/SKILL.md +220 -0
- package/skills/risk-compliance/report-writing/references/audit-report-template.md +121 -0
- package/skills/risk-compliance/report-writing/references/external-assessment-template.md +167 -0
- package/skills/risk-compliance/report-writing/references/management-report-template.md +145 -0
- package/skills/risk-compliance/report-writing/references/vars-report-template.md +224 -0
- package/skills/risk-compliance/report-writing/release-check.mjs +93 -0
- package/skills/risk-compliance/similar-domain/LICENSE +3 -0
- package/skills/risk-compliance/similar-domain/SKILL.md +203 -0
- package/skills/risk-compliance/similar-domain/references/entities.md +67 -0
- package/skills/risk-compliance/similar-domain/references/fingerprints.md +134 -0
- package/skills/risk-compliance/similar-domain/references/pivots.md +82 -0
- package/skills/risk-compliance/similar-domain/references/report-template.md +93 -0
- package/skills/risk-compliance/similar-domain/references/scoring.md +130 -0
- package/skills/risk-compliance/similar-domain/references/terminology.md +41 -0
- package/skills/risk-compliance/similar-domain/references/workflow.md +85 -0
- package/skills/risk-compliance/similar-domain/release-check.mjs +93 -0
- package/skills/risk-compliance/tld-detection/LICENSE +3 -0
- package/skills/risk-compliance/tld-detection/SKILL.md +245 -0
- package/skills/risk-compliance/tld-detection/agents/openai.yaml +9 -0
- package/skills/risk-compliance/tld-detection/references/chinese-polish.md +55 -0
- package/skills/risk-compliance/tld-detection/references/clean.md +51 -0
- package/skills/risk-compliance/tld-detection/references/clustering.md +235 -0
- package/skills/risk-compliance/tld-detection/references/decision.md +189 -0
- package/skills/risk-compliance/tld-detection/references/fx-conversion.md +232 -0
- package/skills/risk-compliance/tld-detection/references/report-template.md +272 -0
- package/skills/risk-compliance/tld-detection/references/streams.md +141 -0
- package/skills/risk-compliance/tld-detection/references/terminology.md +95 -0
- package/skills/risk-compliance/tld-detection/references/workflow.md +95 -0
- package/skills/risk-compliance/tld-detection/release-check.mjs +93 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +390 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +14 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +650 -0
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +14 -0
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +134 -0
- package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +520 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Print the coordination a resolve-issues run owes right now, from its saved state, so the
|
|
3
|
+
// main agent never has to remember follow-on steps.
|
|
4
|
+
//
|
|
5
|
+
// next-actions.mjs --run <run dir> [--capacity-json '<snapshot>' | --slots N] [--quiet-minutes 45] [--resume] [--sync] [--json] [--peek] [--mode full|narrow|off] [--rules] [--exit-due]
|
|
6
|
+
//
|
|
7
|
+
// Reads <run>/task-plan.json (units with status, stage, worker, workerSince, workspace,
|
|
8
|
+
// dependsOn, pr, headSha, reviewedHead) and <run>/reviews/<unit>-r<N>.json (+ .fix.json; its head, complete, and
|
|
9
|
+
// external.awaiting, and whether it holds a P0/P1). A unit with `worker`
|
|
10
|
+
// set is busy. Actions, in priority order:
|
|
11
|
+
//
|
|
12
|
+
// add-unit, drop-unit, set-deps, move-link, blocked-outside, cycle
|
|
13
|
+
// the plan differs from the spec's GitHub issue tree (plan-sync.mjs): apply
|
|
14
|
+
// it through update_plan. --resume and --sync fetch the tree (one GraphQL
|
|
15
|
+
// query) and save it; every other call diffs the saved tree, with no
|
|
16
|
+
// GitHub call. Sync after the run files an issue
|
|
17
|
+
// unblock a unit marked blocked without a reason from BLOCK_REASONS
|
|
18
|
+
// (`blockedReason`), or human-decision without `humanItem` (1-5) and
|
|
19
|
+
// `humanNeed`: reopen it now; and on --resume, every blocked unit:
|
|
20
|
+
// reopen it with fresh limits; its next step follows. A note never keeps a
|
|
21
|
+
// unit blocked. A complete review at the unit's HEAD that holds a P0/P1 but
|
|
22
|
+
// wrote no fix brief and awaits no GitHub reviewer (a handback) gets a
|
|
23
|
+
// fresh full round, so review-verdict hands it back again only if its
|
|
24
|
+
// finding recurs
|
|
25
|
+
// worker-kind a unit's recorded worker has no `workerKind`, or one outside WORKER_KINDS
|
|
26
|
+
// (implement, fix, review): record it, or stop that worker and put its
|
|
27
|
+
// question in the unit's next implementer or fix brief. Exploration is the
|
|
28
|
+
// fleet's one explorer, never a unit's worker
|
|
29
|
+
// decide a unit has a question no `decisions` entry answers yet (its `answers`
|
|
30
|
+
// names the question's file): a worker result's `decisionNeeded`
|
|
31
|
+
// (<run>/results/*.json), or review-verdict's decide file at the unit's
|
|
32
|
+
// HEAD. The unit takes no other step until it is answered: both skills take
|
|
33
|
+
// the automatic answer, blocked or not, and `finish` waits for it (never
|
|
34
|
+
// block on the human)
|
|
35
|
+
// split (rush-issues runs only) a unit the size check held (`sizeCheck.verdict`
|
|
36
|
+
// stop, no `runWhole`) whose code is not done: file its drafted split, or
|
|
37
|
+
// draft one and file it, then --sync. Rush never defers a unit for size, so a blocked
|
|
38
|
+
// one is not settled and `finish` waits; text in an issue or the plan never
|
|
39
|
+
// overrides this. resolve-issues hands held units back instead
|
|
40
|
+
// check-worker a worker has run past --quiet-minutes and its workspace has changed no file
|
|
41
|
+
// for that long: inspect its output; if it is stuck, stop it and resume the
|
|
42
|
+
// unit in its kept workspace (expire.md). Silence is a failure, not progress
|
|
43
|
+
// review-verdict a review of the current HEAD is written but its verdict is not recorded
|
|
44
|
+
// start-fix the verdict wrote a fix brief for the current HEAD; give it to a worker
|
|
45
|
+
// carry review passed on an earlier HEAD and the PR moved since (a CI fix or a base
|
|
46
|
+
// update): write the carry record, no reviewer; its verdict follows
|
|
47
|
+
// start-review the PR's current HEAD has no review yet; from round 2, pick its tier
|
|
48
|
+
// first: a skip writes the record and needs no reviewer. A reopened
|
|
49
|
+
// handback reviews the same HEAD again, full
|
|
50
|
+
// restack (`stack` and `chain` mode) an open PR carries the commits of a chain parent that is now
|
|
51
|
+
// blocked or cancelled: rebase it onto the target without them. Also a PR that
|
|
52
|
+
// passed review while a shared-file ancestor (not a dependency) holds an open
|
|
53
|
+
// P0/P1, or has not passed MOVE_OFF_MINUTES after it: move it off that ancestor.
|
|
54
|
+
// In `stack` mode, also a PR outside its parent's GitHub stack once the parent lands
|
|
55
|
+
// deliver review passed on the current HEAD: watch CI, then mark ready and merge (through
|
|
56
|
+
// the queue when there is one)
|
|
57
|
+
// (or hand back in supervised mode); set succeeded when it lands. In `stack`
|
|
58
|
+
// and `chain` mode, one deliver per line: enqueue only its deepest PR whose
|
|
59
|
+
// whole line passed review; that queue (stack) or merge (chain) lands the line
|
|
60
|
+
// open-pr the unit has pushed commits (headSha) or its code is done, the parents allow it,
|
|
61
|
+
// and no PR is open. In `stack` and `chain` mode the detail names its parent:
|
|
62
|
+
// the open run PR it builds on or shares files with (`files`). In `stack` mode a
|
|
63
|
+
// parent holds one stacked child (a GitHub stack is linear): another child of it
|
|
64
|
+
// gets no open-pr until it lands
|
|
65
|
+
// resume in flight with no worker and code not done: resume it in its workspace
|
|
66
|
+
// fill-slot a pending unit whose dependencies are met
|
|
67
|
+
// retry-blocked everything else is settled and a unit blocked on a cause that can clear by
|
|
68
|
+
// itself (retry-*, timeout-task, spawn-death, external-blocker) is due its next automatic attempt
|
|
69
|
+
// (30m, 2h, 8h after blockedAt): set it in-flight, autoRetries + 1
|
|
70
|
+
// wait the same, but no attempt is due yet: keep a heartbeat, never finish
|
|
71
|
+
// finish every unit is delivered, blocked, cancelled, or not started behind one of
|
|
72
|
+
// those (named in the detail, with units built whose PR cannot open because a
|
|
73
|
+
// parent will not land): write the handoff. For a
|
|
74
|
+
// GitHub spec it needs a fetch in the same call: without --sync, or when the
|
|
75
|
+
// fetch failed, it prints `sync` instead. A run never finishes on a tree it
|
|
76
|
+
// could not read
|
|
77
|
+
//
|
|
78
|
+
// Text output ends with one short line pointing to the skill path. With --resume or --rules it
|
|
79
|
+
// ends with RULES instead, the rules a compaction summary most often drops (in narrow and off
|
|
80
|
+
// modes, a one-line rules reminder for that mode).
|
|
81
|
+
//
|
|
82
|
+
// Only this script decides that the run is finished. End a turn only when it prints nothing
|
|
83
|
+
// due, or `finish`. Each due action records when it was first printed (<run>/next-actions-seen.json;
|
|
84
|
+
// --peek reads without recording), and a repeat says how long it has gone undone.
|
|
85
|
+
//
|
|
86
|
+
// `blocked` means a unit itself is stopped, and its `blockedReason` says why, from
|
|
87
|
+
// BLOCK_REASONS: a failure out of progress, a cause outside the run (external-blocker), a human-only
|
|
88
|
+
// decision, a review handback, or (not in rush runs) a size hold. Any other block is reopened. A unit that only waits on another unit is
|
|
89
|
+
// not started: it stays `pending`, never `blocked`. A pending
|
|
90
|
+
// unit behind a parent that will not finish this run (blocked, cancelled, or `excluded`) gets no
|
|
91
|
+
// action and counts as settled, so `finish` names it as not started and what it waits on.
|
|
92
|
+
//
|
|
93
|
+
// Actions that need a worker (start-fix, start-review, resume, fill-slot) are limited to the
|
|
94
|
+
// free slots. A unit starts when its `dependsOn` parents' code is done, in every delivery mode;
|
|
95
|
+
// in `wait` mode its PR opens once they merge. A `stacksOn` parent (a shared surface) orders
|
|
96
|
+
// only the PR, and only in `stack` and `chain` mode; in `wait` mode PRs merge in the order they
|
|
97
|
+
// are ready. `stack` mode (a live Mergify queue): a PR opens on its parent's branch as soon as its
|
|
98
|
+
// code is done and the parent's PR is open, linked into one GitHub-native stack. `chain` mode (no
|
|
99
|
+
// Mergify, but the merge keeps commits): a PR opens on the target as soon as its parents' PRs are
|
|
100
|
+
// open, carrying its chain parent's commits. Pass --resume on the first call of
|
|
101
|
+
// a new invocation of an existing run.
|
|
102
|
+
//
|
|
103
|
+
// Exit code: 0 whenever the script ran and answered, whatever is due (including `finish` and
|
|
104
|
+
// `wait`); 2 when it could not (bad arguments, unreadable plan). Due or not is in the output:
|
|
105
|
+
// the text lines, and `exitDue` in --json. --exit-due restores the old exit 1 when anything is due,
|
|
106
|
+
// for a caller that branches on it.
|
|
107
|
+
//
|
|
108
|
+
// Capacity is optional. Without --capacity-json and --slots, every worker-needing action is due,
|
|
109
|
+
// each noted as needing a free worker slot (the harness spawn limit governs). With either flag,
|
|
110
|
+
// worker-needing actions are limited to the free slots, as before.
|
|
111
|
+
|
|
112
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
113
|
+
import { dirname, join } from 'node:path'
|
|
114
|
+
import { fileURLToPath } from 'node:url'
|
|
115
|
+
|
|
116
|
+
import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
|
|
117
|
+
import { excludedIds, syncRun } from './plan-sync.mjs'
|
|
118
|
+
import { BLOCKING } from './review-verdict.mjs'
|
|
119
|
+
|
|
120
|
+
// No skill-owned concurrency default: the caller supplies fresh host capacity.
|
|
121
|
+
// The only workers a unit runs (SKILL.md, Your role). A watcher is not a worker.
|
|
122
|
+
export const WORKER_KINDS = Object.freeze(['implement', 'fix', 'review'])
|
|
123
|
+
export const SKILL_PATH = join(dirname(dirname(fileURLToPath(import.meta.url))), 'SKILL.md')
|
|
124
|
+
// Printed after every text call, and by rehydrate.mjs after a compaction.
|
|
125
|
+
export const RULES = 'Per unit, run only implement, fix, and review workers, recorded as workerKind: no planning, architecture, product, or setup workers. Exploration continues the run\'s one explorer, on the fleet explore row. You coordinate: never edit product code. In full mode take every step from next-actions.mjs; in every mode the run ends only when it reports done. A HEAD after a passing review is carried; any other new HEAD takes the tier rereview-scope.mjs picks, never yours. Decide before you defer: a question a worker or review raises is answered and recorded, then the unit continues; retrying or deferring never answers it. Never block on the human: take the automatic answer and keep going. Fix the root cause, never only its symptom. Before each scheduling pass, refresh subagent_settings and pass its snapshot with --capacity-json; do not reuse a run-start budget or subtract workflow workers again.'
|
|
126
|
+
export const DEFAULT_QUIET_MINUTES = 45
|
|
127
|
+
// The reasons a unit may stay blocked (expire.md#reason-codes). Waiting on another unit, an
|
|
128
|
+
// infrastructure or external blocker (a tree issue the unit waits on), and a size hold in a rush
|
|
129
|
+
// run (split instead) are not among them.
|
|
130
|
+
export const BLOCK_REASONS = Object.freeze(['retry-explore', 'retry-implement', 'retry-review', 'retry-ci', 'timeout-task', 'spawn-death', 'external-blocker', 'human-decision', 'review-handback', 'size-hold'])
|
|
131
|
+
// An undone action says how long it has been printed once it is this old.
|
|
132
|
+
export const UNDONE_NOTE_MINUTES = 10
|
|
133
|
+
// expire.md#human-only: a human-decision block names its item (1-5) and what the human must do.
|
|
134
|
+
// Without them the deferral is the run's own work (pi 01a147fb deferred #1747 for a refused tool).
|
|
135
|
+
export const HUMAN_ITEMS = Object.freeze([1, 2, 3, 4, 5])
|
|
136
|
+
export const namesHumanItem = (u) => HUMAN_ITEMS.includes(u.humanItem) && typeof u.humanNeed === 'string' && u.humanNeed.trim() !== ''
|
|
137
|
+
// A PR that passed review waits this long on a shared-file chain ancestor still in review, then moves off it.
|
|
138
|
+
// Replay (skyee-ai-risk, fmm-express, wiki-kb, pi-better-harness): without it, wiki-kb's chained PRs waited 872
|
|
139
|
+
// minutes in all behind slow reviews, more than wait mode's 534; with it, 281.
|
|
140
|
+
export const MOVE_OFF_MINUTES = 30
|
|
141
|
+
const WAITS_ON_OTHERS = new Set(['deliver', 'check-worker'])
|
|
142
|
+
const TERMINAL = new Set(['succeeded', 'blocked', 'cancelled'])
|
|
143
|
+
// How much the text output prescribes; JSON output is the same in every mode.
|
|
144
|
+
export const SCHEDULER_MODES = Object.freeze(['full', 'narrow', 'off'])
|
|
145
|
+
// Blocks that can clear without a human, and the minutes before each automatic fresh attempt.
|
|
146
|
+
const AUTO_RETRY = new Set(['retry-ci', 'timeout-task', 'spawn-death', 'external-blocker'])
|
|
147
|
+
const AUTO_RETRY_GAPS_MIN = [30, 120, 480]
|
|
148
|
+
const NEEDS_SLOT = new Set(['restack', 'start-fix', 'start-review', 'resume', 'fill-slot'])
|
|
149
|
+
|
|
150
|
+
export class NextActionsError extends Error {}
|
|
151
|
+
export const RUN_SKILLS = Object.freeze(['resolve-issues', 'rush-issues'])
|
|
152
|
+
|
|
153
|
+
export function normalizeCapacity(input) {
|
|
154
|
+
if (input === undefined || input === null) return { source: 'none' }
|
|
155
|
+
if (typeof input === 'number') {
|
|
156
|
+
if (!Number.isSafeInteger(input) || input < 0) throw new NextActionsError('--slots must be a nonnegative safe integer')
|
|
157
|
+
return { source: 'explicit', slots: input }
|
|
158
|
+
}
|
|
159
|
+
const fields = ['maxConcurrent', 'runningCount', 'pendingCount', 'occupiedSlots', 'availableSlots']
|
|
160
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) ||
|
|
161
|
+
fields.some(key => !Number.isSafeInteger(input[key]) || input[key] < 0) || input.maxConcurrent < 1) {
|
|
162
|
+
throw new NextActionsError('capacity requires a fresh Harness snapshot with maxConcurrent, runningCount, pendingCount, occupiedSlots, and availableSlots; or an explicit --slots budget')
|
|
163
|
+
}
|
|
164
|
+
const occupied = input.runningCount + input.pendingCount
|
|
165
|
+
if (!Number.isSafeInteger(occupied) || input.occupiedSlots !== occupied ||
|
|
166
|
+
input.availableSlots !== Math.max(0, input.maxConcurrent - occupied)) {
|
|
167
|
+
throw new NextActionsError('inconsistent Harness capacity: occupiedSlots must equal runningCount + pendingCount, and availableSlots must equal max(0, maxConcurrent - occupiedSlots)')
|
|
168
|
+
}
|
|
169
|
+
if (input.revision !== undefined && (typeof input.revision !== 'string' || !input.revision.trim())) {
|
|
170
|
+
throw new NextActionsError('capacity revision must be a nonempty string when supplied')
|
|
171
|
+
}
|
|
172
|
+
return { source: 'harness', ...(input.revision === undefined ? {} : { revision: input.revision }),
|
|
173
|
+
...Object.fromEntries(fields.map(key => [key, input[key]])) }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const hasWorker = (u) => u.worker !== undefined && u.worker !== null && u.worker !== ''
|
|
177
|
+
const codeDone = (u) => u.stage === 'done'
|
|
178
|
+
|
|
179
|
+
// Pure decision from the plan and each unit's newest review round.
|
|
180
|
+
// `capacity`: a fresh Harness snapshot, or an explicitly reconciled total workflow budget.
|
|
181
|
+
// Snapshot availableSlots is already net of all running and reserved workers.
|
|
182
|
+
// `reviews`: { [unitId]: { round, at, head, fix, complete, awaiting, blocking } } for the newest round
|
|
183
|
+
// (`at`: when its file was written, ms).
|
|
184
|
+
// `resume`: this is the first call of a new invocation, so earlier blocks get a fresh attempt.
|
|
185
|
+
// `now` (ms) and `activity` ({ [unitId]: newest workspace change, ms }) find silent workers;
|
|
186
|
+
// without `now`, no worker is checked. `sync` is plan-sync.mjs's result for this call, or
|
|
187
|
+
// null when the spec is not a GitHub issue tree.
|
|
188
|
+
// `seen` ({ "<action>:<unit>": ms }) is when each action was first printed; the result's `seen`
|
|
189
|
+
// is the map to save for the next call.
|
|
190
|
+
// `questions` ({ [unitId]: [{ key, ask }] }, from readQuestions) are the workers' open questions;
|
|
191
|
+
// `key` is the result file, relative to the run, that a `decisions` entry's `answers` names.
|
|
192
|
+
export function nextActions(plan, reviews = {}, capacity, { resume = false, now, activity = {}, quietMinutes = DEFAULT_QUIET_MINUTES, sync = null, seen = {}, questions = {} } = {}) {
|
|
193
|
+
const effectiveCapacity = normalizeCapacity(capacity)
|
|
194
|
+
const saved = (Array.isArray(plan?.units) ? plan.units : plan?.issues) ?? []
|
|
195
|
+
if (!Array.isArray(saved) || saved.length === 0) throw new NextActionsError('task plan has no units')
|
|
196
|
+
const excluded = excludedIds(plan)
|
|
197
|
+
const gates = (u) => (u.dependsOn ?? []).map(String).filter((d) => excluded.has(d))
|
|
198
|
+
// In a rush run, a size-held unit is split, never deferred: it gets `split` until the sync
|
|
199
|
+
// drops it (split into sub-issues) or a rerun size check clears it.
|
|
200
|
+
const shortcut = plan.skill === 'rush-issues'
|
|
201
|
+
const sizeHeld = (u) => shortcut && (u.sizeCheck?.verdict ?? u.sizeCheck?.scope) === 'stop' && u.sizeCheck?.runWhole !== true && !codeDone(u)
|
|
202
|
+
&& !hasWorker(u) && u.status !== 'succeeded' && u.status !== 'cancelled' && gates(u).length === 0
|
|
203
|
+
&& !excluded.has(String(u.id))
|
|
204
|
+
const reasonOk = (u) => BLOCK_REASONS.includes(u.blockedReason) && !(shortcut && u.blockedReason === 'size-hold')
|
|
205
|
+
&& (u.blockedReason !== 'human-decision' || namesHumanItem(u))
|
|
206
|
+
const reopen = (u) => (resume || !reasonOk(u)) && u.status === 'blocked' && !hasWorker(u) && gates(u).length === 0 && !sizeHeld(u)
|
|
207
|
+
// `stack` and `chain` both open a child's PR once its parents' PRs are open; only the PR base differs.
|
|
208
|
+
if (plan.deliveryMode !== undefined && !['stack', 'chain', 'wait'].includes(plan.deliveryMode)) throw new NextActionsError(`deliveryMode must be stack, chain, or wait: ${JSON.stringify(plan.deliveryMode)}`)
|
|
209
|
+
const chain = plan.deliveryMode === 'chain'
|
|
210
|
+
// `stack` and `chain` share the line logic: parents from shared files, parentHead, offLine, move-off,
|
|
211
|
+
// and one delivery per line. They differ only in the PR base and how a line is enqueued.
|
|
212
|
+
const linked = plan.deliveryMode !== 'wait'
|
|
213
|
+
const stack = plan.deliveryMode !== 'wait'
|
|
214
|
+
// `stack` is a GitHub-native stack: a line of PRs, each based on its parent's branch. It is linear.
|
|
215
|
+
const native = plan.deliveryMode === 'stack'
|
|
216
|
+
const savedById = new Map(saved.map((u) => [String(u.id), u]))
|
|
217
|
+
const parentDone = (p) => codeDone(p) || p.status === 'succeeded'
|
|
218
|
+
// A unit wrongly marked blocked while it only waited on its parents reopens as not started.
|
|
219
|
+
// Its kept workspace and commits stay; it resumes there once its parents' code is done.
|
|
220
|
+
const unmetParents = (u) => (u.dependsOn ?? []).map(String).filter((d) => { const p = savedById.get(d); return p && !parentDone(p) })
|
|
221
|
+
const reopenAs = (u) => (!codeDone(u) && unmetParents(u).length ? 'pending' : 'in-flight')
|
|
222
|
+
// A block without a listed reason, behind an excluded issue, is only waiting: not started.
|
|
223
|
+
const units = saved.map((u) => (reopen(u) ? { ...u, status: reopenAs(u) }
|
|
224
|
+
: u.status === 'blocked' && !reasonOk(u) && gates(u).length ? { ...u, status: 'pending' } : u))
|
|
225
|
+
const byId = new Map(units.map((u) => [String(u.id), u]))
|
|
226
|
+
// The first unit or excluded issue up the dependsOn chain that will not finish in this run.
|
|
227
|
+
const waitsOn = (u, seen = new Set()) => {
|
|
228
|
+
for (const d of (u.dependsOn ?? []).map(String)) {
|
|
229
|
+
if (seen.has(d)) continue
|
|
230
|
+
seen.add(d)
|
|
231
|
+
if (excluded.has(d)) return d
|
|
232
|
+
const p = byId.get(d)
|
|
233
|
+
if (!p) continue
|
|
234
|
+
if (p.status === 'blocked' || p.status === 'cancelled') return d
|
|
235
|
+
const up = waitsOn(p, seen)
|
|
236
|
+
if (up) return up
|
|
237
|
+
}
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
240
|
+
const notStarted = (u) => (u.status === 'pending' || u.status === undefined) && !hasWorker(u) && waitsOn(u) !== null
|
|
241
|
+
// Built on a parent's code, but the parent will not land this run: its PR cannot open.
|
|
242
|
+
const builtWaiting = (u) => u.status === 'in-flight' && codeDone(u) && (!u.pr || plan.deliveryMode !== 'wait') && !hasWorker(u) && waitsOn(u) !== null
|
|
243
|
+
// A block counts as settled only with a listed reason, or behind an excluded issue.
|
|
244
|
+
const settledBlock = (u) => u.status !== 'blocked' || reasonOk(u) || gates(u).length > 0
|
|
245
|
+
const supervised = plan.mergeMode === 'supervised'
|
|
246
|
+
const lookup = (ids) => (ids ?? []).map((p) => byId.get(String(p))).filter(Boolean)
|
|
247
|
+
const dependsOnOf = (u) => lookup(u.dependsOn)
|
|
248
|
+
// Two units stacked on each other: the lower id is the parent, so one PR can open first.
|
|
249
|
+
const lowerId = (a, b) => (/^\d+$/.test(a) && /^\d+$/.test(b) ? Number(a) < Number(b) : a < b)
|
|
250
|
+
// A blocked or cancelled stacksOn parent drops out: the PR bases on the target instead. In wait
|
|
251
|
+
// mode a shared surface orders nothing: whichever PR merges second rebases.
|
|
252
|
+
const stackParents = (u) => (stack ? lookup(u.stacksOn) : []).filter((p) => p.status !== 'blocked' && p.status !== 'cancelled')
|
|
253
|
+
.filter((p) => !(p.stacksOn ?? []).map(String).includes(String(u.id)) || lowerId(String(p.id), String(u.id)))
|
|
254
|
+
const parentsOf = (u) => [...lookup(u.dependsOn), ...stackParents(u)]
|
|
255
|
+
// The one parent relation every line walk uses: a reciprocal pair resolves to the lower id.
|
|
256
|
+
const resolvedParent = (u) => { if (!stack || !u.stacksOn?.length) return null; const p = byId.get(String(u.stacksOn[0])); return p && !((p.stacksOn ?? []).map(String).includes(String(u.id)) && !lowerId(String(p.id), String(u.id))) ? p : null }
|
|
257
|
+
// Chain mode: the unmerged units whose commits a unit's branch carries (itself, its dependsOn
|
|
258
|
+
// parents, and its chain parents, transitively).
|
|
259
|
+
const lineOf = (u, out = new Set()) => {
|
|
260
|
+
const id = String(u.id)
|
|
261
|
+
if (out.has(id) || u.status === 'succeeded') return out
|
|
262
|
+
out.add(id)
|
|
263
|
+
for (const p of [...lookup(u.dependsOn), resolvedParent(u)].filter(Boolean)) lineOf(p, out)
|
|
264
|
+
return out
|
|
265
|
+
}
|
|
266
|
+
const depsOf = (u, out = new Set()) => { for (const p of lookup(u.dependsOn)) if (!out.has(String(p.id))) { out.add(String(p.id)); depsOf(p, out) } return out }
|
|
267
|
+
const ordered = (ids) => [...ids].map((i) => byId.get(i)).sort((a, b) => lineOf(a).size - lineOf(b).size).map((v) => String(v.id))
|
|
268
|
+
const openRunPr = (v) => Boolean(v.pr) && !TERMINAL.has(v.status)
|
|
269
|
+
// The open run PR a finished unit chains on: of the open PRs it carries (dependsOn) or shares a
|
|
270
|
+
// file with, the one whose line holds all its dependencies, then most of them; a tie goes to the
|
|
271
|
+
// deeper line, then the higher id.
|
|
272
|
+
const chainParent = (u) => {
|
|
273
|
+
const files = new Set(u.files)
|
|
274
|
+
const deps = lineOf(u)
|
|
275
|
+
const shares = (v) => (v.files ?? []).filter((f) => files.has(f))
|
|
276
|
+
const cands = units.filter((v) => String(v.id) !== String(u.id) && openRunPr(v) && (deps.has(String(v.id)) || shares(v).length > 0))
|
|
277
|
+
if (!cands.length) return null
|
|
278
|
+
const ids = new Set(cands.map((v) => String(v.id)))
|
|
279
|
+
// A line that carries every open dependency comes first; shared files only break ties.
|
|
280
|
+
const depIds = cands.filter((v) => deps.has(String(v.id))).map((v) => String(v.id))
|
|
281
|
+
const coversDeps = (v) => depIds.filter((i) => lineOf(v).has(i)).length
|
|
282
|
+
const covers = (v) => [...lineOf(v)].filter((i) => ids.has(i)).length
|
|
283
|
+
const best = [...cands].sort((a, b) => coversDeps(b) - coversDeps(a) || covers(b) - covers(a) || lineOf(b).size - lineOf(a).size || (lowerId(String(a.id), String(b.id)) ? 1 : -1))[0]
|
|
284
|
+
const line = lineOf(best)
|
|
285
|
+
return { parent: best, shared: shares(best), uncovered: cands.filter((v) => !line.has(String(v.id))) }
|
|
286
|
+
}
|
|
287
|
+
// A GitHub stack is linear: a parent PR holds one stacked child. Another child of it opens once it
|
|
288
|
+
// lands. The child that opened first holds it; of two already open on it, the lower id. A blocked or
|
|
289
|
+
// cancelled child still sits in the GitHub stack, so it still holds the parent.
|
|
290
|
+
const holds = (v, p) => v !== p && Boolean(v.pr) && resolvedParent(v) === p
|
|
291
|
+
const claimed = new Set()
|
|
292
|
+
// The parent each stacked open-pr links onto this pass.
|
|
293
|
+
const openOn = new Map()
|
|
294
|
+
// A PR moved to the target leaves its GitHub stack: take the stack apart and relink what remains.
|
|
295
|
+
// No line touching it is delivered in that pass (`leaving`): unstack leaves a queued PR stacked.
|
|
296
|
+
const leaving = new Set()
|
|
297
|
+
const above = (u) => { const out = []; for (let v = u, n = 0; v && n < units.length; n++) { v = units.filter((w) => w.status !== 'succeeded' && holds(w, v)).sort((a, b) => (lowerId(String(a.id), String(b.id)) ? -1 : 1))[0]; if (!v || out.includes(v)) break; out.push(v) } return out }
|
|
298
|
+
// One topology change per stack per pass: a second PR leaving it waits for the next pass's plan.
|
|
299
|
+
const touchesLeaving = (u) => [...leaving].some((i) => { const v = byId.get(i); return v === u || lineOf(v).has(String(u.id)) || lineOf(u).has(i) })
|
|
300
|
+
const relink = (u) => {
|
|
301
|
+
if (!native) return ''
|
|
302
|
+
leaving.add(String(u.id))
|
|
303
|
+
const below = stackOf(resolvedParent(u)).map((v) => v.pr)
|
|
304
|
+
const top = [u, ...above(u)].map((v) => v.pr)
|
|
305
|
+
const links = [below, top].filter((l) => l.length > 1).map((l) => `gh stack link ${l.join(' ')}`)
|
|
306
|
+
return `; it leaves its GitHub stack: before any PR in that stack is queued (if one is, wait until it lands), gh stack checkout ${u.pr}, gh stack unstack, then ${links.length ? links.join(' and ') : 'nothing to relink'} (bottom first)`
|
|
307
|
+
}
|
|
308
|
+
const heldBy = (p, u) => units.find((v) => v !== u && holds(v, p) && (!u.pr || lowerId(String(v.id), String(u.id))))
|
|
309
|
+
const taken = (p, u) => native && (claimed.has(String(p.id)) || Boolean(heldBy(p, u)))
|
|
310
|
+
// An open PR stacked on a parent another child holds is outside that GitHub stack: it ships after the parent lands.
|
|
311
|
+
// The open PRs of a GitHub stack, bottom first, up to p.
|
|
312
|
+
const stackOf = (p) => { const out = []; for (let v = p; v && v.status !== 'succeeded' && !out.includes(v); v = resolvedParent(v)) out.unshift(v); return out }
|
|
313
|
+
const forked = (u) => { if (!native || !u.pr) return null; const p = resolvedParent(u); return p && heldBy(p, u) ? p : null }
|
|
314
|
+
// A dependsOn cycle among unsettled units never starts: name it instead of idling.
|
|
315
|
+
const cycleFrom = (u) => {
|
|
316
|
+
const path = []
|
|
317
|
+
const walk = (v) => {
|
|
318
|
+
const id = String(v.id)
|
|
319
|
+
const at = path.indexOf(id)
|
|
320
|
+
if (at >= 0) return path.slice(at)
|
|
321
|
+
if (TERMINAL.has(v.status)) return null
|
|
322
|
+
path.push(id)
|
|
323
|
+
for (const p of lookup(v.dependsOn)) { const c = walk(p); if (c) return c }
|
|
324
|
+
path.pop()
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
return walk(u)
|
|
328
|
+
}
|
|
329
|
+
const actions = []
|
|
330
|
+
const add = (action, u, detail) => actions.push({ action, unit: String(u.id), detail })
|
|
331
|
+
const chainReady = []
|
|
332
|
+
const chainOpen = (u) => {
|
|
333
|
+
const id = String(u.id)
|
|
334
|
+
if (!Array.isArray(u.files)) return `record unit ${id}'s own changed files as files (git diff --name-only <its base>...HEAD in its workspace), then rerun: its chain parent comes from them`
|
|
335
|
+
const c = chainParent(u)
|
|
336
|
+
if (!c) return `open unit ${id}'s PR as a draft on the target: no open run PR shares its files${dependsOnOf(u).length ? '; rebase it onto the target first, since its parents merged' : ''}`
|
|
337
|
+
const headless = [c.parent, ...c.uncovered].filter((v) => !v.headSha)
|
|
338
|
+
if (headless.length) return `record headSha for ${headless.map((v) => `unit ${v.id}'s PR`).join(', ')}, then rerun: unit ${id} chains on a parent's exact HEAD`
|
|
339
|
+
const p = c.parent
|
|
340
|
+
if (taken(p, u)) return null
|
|
341
|
+
if (native) { claimed.add(String(p.id)); openOn.set(id, p) }
|
|
342
|
+
const why = c.shared.length ? `shares ${c.shared.slice(0, 3).join(', ')}${c.shared.length > 3 ? ` and ${c.shared.length - 3} more` : ''}` : 'it builds on its code'
|
|
343
|
+
// A dependency on another line rides this branch as a copy: open now so review and CI start, but
|
|
344
|
+
// ship only after that dependency lands and a restack drops the copy (its own PR merges it).
|
|
345
|
+
const deps = lineOf(u)
|
|
346
|
+
const offLine = c.uncovered.filter((v) => deps.has(String(v.id))).map((v) => String(v.id))
|
|
347
|
+
const sharing = c.uncovered.filter((v) => !deps.has(String(v.id)))
|
|
348
|
+
const other = `; set offLine to [${offLine.join(', ')}]${offLine.length ? ': those dependencies sit on another line, so this PR ships only after they land and a restack drops their commits' : ''}${sharing.length ? `; it also shares files with ${sharing.map((v) => v.id).join(', ')} on another line: whichever of those merges second rebases onto the target` : ''}`
|
|
349
|
+
const base = chain ? `the PR keeps base targetBranch and carries ${p.id}'s line until it lands` : `open it with gh pr create --draft --base ${p.branch ?? `<unit ${p.id}'s branch>`}, then link the line as one GitHub stack, bottom first: gh stack link ${[...stackOf(p).map((v) => v.pr), 'the new PR'].join(' ')} (add --base <targetBranch> when it is not the default branch; in a repository with Mergify's stack hooks, push with the mergify skill's stack-push.mjs instead)`
|
|
350
|
+
return `open unit ${id}'s PR as a draft ${chain ? 'on the target, chained' : 'stacked'} on unit ${p.id} (${p.pr}; ${why}): rebase onto ${p.branch ?? `unit ${p.id}'s branch`} at ${p.headSha ? p.headSha.slice(0, 7) : 'its HEAD'} now (a fix worker resolves conflicts once), set stacksOn to [${p.id}] and parentHead to that HEAD, and review it against that branch; ${base}${other}`
|
|
351
|
+
}
|
|
352
|
+
// Decide before you defer: an open question holds the unit until a decision answers it.
|
|
353
|
+
const answeredBy = new Map()
|
|
354
|
+
for (const d of Array.isArray(plan.decisions) ? plan.decisions : []) {
|
|
355
|
+
for (const key of [d?.answers].flat()) if (typeof key === 'string' && key) answeredBy.set(key, String(d.id ?? ''))
|
|
356
|
+
}
|
|
357
|
+
const decideKey = (id, r) => `reviews/${id}-r${r.round}.decide.json`
|
|
358
|
+
const reviewQuestion = (u) => { const r = reviews[String(u.id)]; return r?.decide && r.head === u.headSha && !r.fix ? r : null }
|
|
359
|
+
const openQuestions = (u) => {
|
|
360
|
+
const id = String(u.id)
|
|
361
|
+
const out = (questions[id] ?? []).filter((q) => !answeredBy.has(q.key))
|
|
362
|
+
const r = reviewQuestion(u)
|
|
363
|
+
if (r && !answeredBy.has(decideKey(id, r))) out.push({ key: decideKey(id, r), ask: r.ask || 'a recurring spec finding: implementer and reviewer read the requirement differently' })
|
|
364
|
+
return out
|
|
365
|
+
}
|
|
366
|
+
const deciding = (u) => !hasWorker(u) && u.status !== 'succeeded' && u.status !== 'cancelled' && openQuestions(u).length > 0
|
|
367
|
+
// A handback at the unit's HEAD: the review is complete and holds a P0/P1, yet wrote no fix brief
|
|
368
|
+
// and waits on no GitHub reviewer. Review it again instead of re-reading the old verdict.
|
|
369
|
+
const reviewAgain = new Set()
|
|
370
|
+
for (const u of saved) {
|
|
371
|
+
// An open question's `decide` already sets the unit in-flight or defers it with its item.
|
|
372
|
+
if (!reopen(u) || (deciding(u) && u.blockedReason === 'human-decision' && !resume)) continue
|
|
373
|
+
const r = reviews[String(u.id)]
|
|
374
|
+
if (r && u.headSha && r.head === u.headSha && u.reviewedHead !== u.headSha && !r.fix && !r.decide && r.complete && r.blocking && !r.awaiting) reviewAgain.add(String(u.id))
|
|
375
|
+
const why = reasonOk(u) ? ''
|
|
376
|
+
: u.blockedReason === 'human-decision' ? `unit ${u.id} is deferred to a human without naming its human-only item (humanItem 1-5 and humanNeed, expire.md#human-only), so its fix is the run's own: `
|
|
377
|
+
: `unit ${u.id} is blocked without a reason from expire.md's list (blockedReason ${JSON.stringify(u.blockedReason ?? null)}), so it is not blocked: `
|
|
378
|
+
add('unblock', u, reopenAs(u) === 'pending'
|
|
379
|
+
? `${why}unit ${u.id} only waits on ${unmetParents(u).join(', ')}: set it pending (not started), keep its workspace, and let it start when they are done`
|
|
380
|
+
: why ? `${why}set it in-flight and take its next step; a wait on another unit or an outside blocker is pending, and a size hold in a rush run is split`
|
|
381
|
+
: `unit ${u.id} was blocked in an earlier invocation: set it in-flight with fresh limits and take its failure's response again (expire.md)`)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (Number.isFinite(now)) {
|
|
385
|
+
const quietMs = quietMinutes * 60_000
|
|
386
|
+
for (const u of units) {
|
|
387
|
+
if (!hasWorker(u)) continue
|
|
388
|
+
const since = Date.parse(u.workerSince ?? '')
|
|
389
|
+
if (!Number.isFinite(since)) continue
|
|
390
|
+
const last = Math.max(since, activity[String(u.id)] ?? 0)
|
|
391
|
+
const quiet = Math.floor((now - last) / 60_000)
|
|
392
|
+
if (now - last > quietMs) add('check-worker', u, `unit ${u.id}'s worker ${u.worker} has changed nothing in its workspace for ${quiet}m: read its latest output; if it is stuck, stop it and resume the unit in its kept workspace (expire.md)`)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (const u of units) {
|
|
397
|
+
if (!hasWorker(u) || WORKER_KINDS.includes(u.workerKind)) continue
|
|
398
|
+
add('worker-kind', u, u.workerKind === undefined || u.workerKind === null
|
|
399
|
+
? `unit ${u.id}'s worker ${u.worker} has no workerKind: set it to implement, fix, or review; any other worker for a unit stops`
|
|
400
|
+
: `unit ${u.id}'s worker ${u.worker} is a ${JSON.stringify(u.workerKind)}: a unit runs only implement, fix, and review workers. Stop it, keep what it wrote, put its question in the unit's next implementer or fix brief, and clear worker; an explorer belongs on the fleet explore row`)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const u of units) {
|
|
404
|
+
if (!deciding(u)) continue
|
|
405
|
+
const open = openQuestions(u)
|
|
406
|
+
const asks = open.map((q) => `${q.key}: ${q.ask}`).join('; ')
|
|
407
|
+
const keys = open.map((q) => JSON.stringify(q.key)).join(', ')
|
|
408
|
+
add('decide', u, `unit ${u.id} needs a decision (${asks}). Take the automatic answer (preflight.md#never-block-on-the-human): accept the recommendation, or form one that keeps every acceptance criterion, is easiest to undo, and touches least outside the unit. Record it in decisions with answers ${keys} and source agent-recommendation, then take the unit's next step${u.status === 'blocked' ? ' (set it in-flight)' : ''}. Defer only when every answer loosens an acceptance criterion, a required check, or a merge rule, or needs access the run lacks: record that as the decision and block the unit (human-decision, with its humanItem and humanNeed)`)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const u of units) {
|
|
412
|
+
if (deciding(u)) continue
|
|
413
|
+
if (sizeHeld(u)) { add('split', u, `unit ${u.id} is held by the size check: rush never defers for size. File its drafted split, or draft one whose largest slice is smaller than the unit and file it; a unit held only by a question takes the recommended answer (preflight.md#shortcut-mode). Then rerun the size check, or --sync after filing`); continue }
|
|
414
|
+
// A unit with pushed commits gets its draft PR now, even while its worker runs, so CI runs and
|
|
415
|
+
// the work is safe while it finishes; review still waits for its code to be done.
|
|
416
|
+
const earlyPr = (v) => !TERMINAL.has(v.status) && !codeDone(v) && !v.pr && v.headSha && waitsOn(v) === null
|
|
417
|
+
&& parentsOf(v).every((p) => (stack && !native ? Boolean(p.pr) || p.status === 'succeeded' : p.status === 'succeeded'))
|
|
418
|
+
if (earlyPr(u)) add('open-pr', u, `open unit ${u.id}'s PR as a draft now at ${String(u.headSha).slice(0, 7)} (implementation.md#open-the-pr for this delivery mode): CI runs while it finishes; start review once its code is done`)
|
|
419
|
+
if (TERMINAL.has(u.status) || hasWorker(u)) continue
|
|
420
|
+
const id = String(u.id)
|
|
421
|
+
const gate = gates(u)
|
|
422
|
+
if (gate.length) continue
|
|
423
|
+
if (u.status === 'pending' || u.status === undefined) {
|
|
424
|
+
const met = dependsOnOf(u).every(parentDone)
|
|
425
|
+
if (met) add('fill-slot', u, u.workspace || u.headSha ? `start unit ${id} in its kept workspace: its dependencies are met` : `start unit ${id}: its dependencies are met`)
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
if (!codeDone(u)) { add('resume', u, `unit ${id} is in flight with no worker; resume it in its workspace`); continue }
|
|
429
|
+
// A parent that will not land holds its child, PR or not: it is settled, never opened or moved.
|
|
430
|
+
if (waitsOn(u) !== null && (!u.pr || linked)) continue
|
|
431
|
+
if (!u.pr) {
|
|
432
|
+
const ok = parentsOf(u).every((p) => (stack ? Boolean(p.pr) || p.status === 'succeeded' : p.status === 'succeeded'))
|
|
433
|
+
if (ok && linked) { const d = chainOpen(u); if (d) add('open-pr', u, d) }
|
|
434
|
+
else if (ok) add('open-pr', u, stack || dependsOnOf(u).length === 0 ? `open unit ${id}'s PR as a draft` : `open unit ${id}'s PR as a draft: rebase it onto the target first, since its parents merged`)
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
const dropped = linked ? lookup(u.stacksOn).filter((p) => (p.status === 'blocked' || p.status === 'cancelled') && !(u.dependsOn ?? []).map(String).includes(String(p.id))) : []
|
|
438
|
+
if (dropped.length) {
|
|
439
|
+
if (!(native && touchesLeaving(u))) add('restack', u, `unit ${id}'s PR carries the commits of chain parent ${dropped.map((p) => p.id).join(', ')}, which will not land: rebase it onto the target without them (a fix worker resolves conflicts), drop ${dropped.map((p) => p.id).join(', ')} from stacksOn, push${native ? ', retarget the PR to the target' : ''}; the new HEAD takes the tier the scope script picks${relink(u)}`)
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
if (!u.headSha) continue
|
|
443
|
+
const review = reviews[id]
|
|
444
|
+
// A fix brief at the HEAD (GitHub feedback merged after the pass) outranks the old pass.
|
|
445
|
+
const fixAtHead = review && review.head === u.headSha && review.fix
|
|
446
|
+
if (u.reviewedHead === u.headSha && !fixAtHead) {
|
|
447
|
+
if (linked) { chainReady.push(u); continue }
|
|
448
|
+
add('deliver', u, supervised
|
|
449
|
+
? `review passed on ${u.headSha.slice(0, 7)}: when CI is green, hand the PR back merge-ready`
|
|
450
|
+
: `review passed on ${u.headSha.slice(0, 7)}: when CI is green, mark ready and merge (enqueue under a queue); set succeeded when it lands`)
|
|
451
|
+
} else if (reviewAgain.has(id)) {
|
|
452
|
+
add('start-review', u, `review ${u.pr} at ${u.headSha.slice(0, 7)} again against the issue as it reads now (round ${review.round + 1}, full); review-verdict compares it with round ${review.round}`)
|
|
453
|
+
} else if (review && u.reviewedHead === review.head && review.complete && !review.blocking && !review.awaiting) {
|
|
454
|
+
add('carry', u, `review passed on ${review.head.slice(0, 7)}; ${u.headSha.slice(0, 7)} came after it, so no reviewer: in its review clone run review-verdict.mjs --carry --review <run>/reviews/${id}-r${review.round + 1}.json --head ${u.headSha} --base <review base> ; then github-reviews.mjs merge and review-verdict.mjs on that file`)
|
|
455
|
+
} else if (review && review.head === u.headSha) {
|
|
456
|
+
const answer = reviewQuestion(u) ? answeredBy.get(decideKey(id, review)) : undefined
|
|
457
|
+
if (review.fix) add('start-fix', u, `give reviews/${id}-r${review.round}.fix.json to a fix worker, as its action says`)
|
|
458
|
+
else if (answer !== undefined) add('review-verdict', u, `decision ${answer} answers reviews/${id}-r${review.round}.decide.json: run review-verdict.mjs on reviews/${id}-r${review.round}.json with --decision ${answer}`)
|
|
459
|
+
else add('review-verdict', u, `run github-reviews.mjs merge, then review-verdict.mjs, on reviews/${id}-r${review.round}.json`)
|
|
460
|
+
} else {
|
|
461
|
+
const next = (review?.round ?? 0) + 1
|
|
462
|
+
add('start-review', u, review
|
|
463
|
+
? `pick the tier for ${u.pr} at ${u.headSha.slice(0, 7)}: rereview-scope.mjs --prior <run>/reviews/${id}-r${review.round}.json --base <review base> --head ${u.headSha} --write-skip <run>/reviews/${id}-r${next}.json; on skip run review-verdict.mjs on that file, else spawn round ${next}'s reviewer`
|
|
464
|
+
: `review ${u.pr} at ${u.headSha.slice(0, 7)} (round ${next})`)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Chain mode: deliver each line once, at its deepest PR whose whole line passed review and is built on
|
|
469
|
+
// its parents' current HEADs (`parentHead`): a branch built on an older parent HEAD would land commits
|
|
470
|
+
// nobody reviewed. When a parent's HEAD passes, restack each child built on an older one, once.
|
|
471
|
+
const passed = new Set(chainReady.map((u) => String(u.id)))
|
|
472
|
+
const parentOf = resolvedParent
|
|
473
|
+
const stale = (u) => { const p = parentOf(u); return Boolean(p && p.status !== 'succeeded' && u.parentHead !== p.headSha) }
|
|
474
|
+
const consistent = (u) => { for (let v = u, n = 0; v && n <= units.length; v = parentOf(v), n++) if (stale(v)) return false; return true }
|
|
475
|
+
// A PR carrying copies of another line's dependency ships only once they landed and a restack dropped
|
|
476
|
+
// them. What it carries is derived, never trusted: every unmerged dependency off its chain line, plus
|
|
477
|
+
// the recorded `offLine` ids that are dependencies (copies not yet dropped).
|
|
478
|
+
const chainSet = (u) => { const out = new Set(); let v = parentOf(u); for (let n = 0; v && n <= units.length; n++) { for (const i of lineOf(v)) out.add(i); v = parentOf(v) } return out }
|
|
479
|
+
const offLineOf = (u) => {
|
|
480
|
+
if (!linked || !u.pr) return []
|
|
481
|
+
const deps = depsOf(u)
|
|
482
|
+
const onLine = chainSet(u)
|
|
483
|
+
const derived = [...deps].filter((i) => byId.get(i)?.status !== 'succeeded' && !onLine.has(i))
|
|
484
|
+
return [...new Set([...derived, ...(u.offLine ?? []).map(String).filter((i) => deps.has(i))])]
|
|
485
|
+
}
|
|
486
|
+
const cleaning = new Set()
|
|
487
|
+
// A chained PR with no recorded offLine may carry copies nobody can see any more: restack it first.
|
|
488
|
+
const unrecorded = (u) => linked && Boolean(u.pr) && Boolean(u.stacksOn?.length) && !Array.isArray(u.offLine)
|
|
489
|
+
if (linked) {
|
|
490
|
+
for (const u of units) {
|
|
491
|
+
const p = forked(u)
|
|
492
|
+
if (!p || p.status !== 'succeeded' || TERMINAL.has(u.status) || hasWorker(u)) continue
|
|
493
|
+
cleaning.add(String(u.id))
|
|
494
|
+
add('restack', u, `unit ${p.id} landed, and unit ${u.id}'s PR sat outside its GitHub stack (unit ${heldBy(p, u).id} held it): rebase it onto the target, clear stacksOn and parentHead, retarget the PR to the target, and push, so CI runs on the new base; carry a passed review, else the scope script picks the tier`)
|
|
495
|
+
}
|
|
496
|
+
for (const u of units) {
|
|
497
|
+
if (cleaning.has(String(u.id))) continue
|
|
498
|
+
if (!unrecorded(u) || TERMINAL.has(u.status) || hasWorker(u) || waitsOn(u) !== null) continue
|
|
499
|
+
cleaning.add(String(u.id))
|
|
500
|
+
const p = parentOf(u)
|
|
501
|
+
if (native && p && TERMINAL.has(p.status) && p.status !== 'succeeded' && touchesLeaving(u)) continue
|
|
502
|
+
add('restack', u, `unit ${u.id} has no recorded offLine: rebase it onto ${p && !TERMINAL.has(p.status) ? `unit ${p.id}'s HEAD ${p.headSha ? p.headSha.slice(0, 7) : ''}`.trim() + `, set parentHead to ${p.headSha ? p.headSha.slice(0, 7) : 'that HEAD'}` : 'the target, clear stacksOn and parentHead'}, keep only its own commits and its dependencies on that line, record offLine as the dependencies it still carries from other lines (often []), push, and cancel CI on the replaced HEAD; carry a passed review, else the scope script picks the tier${p && p.status === 'succeeded' ? '' : p && TERMINAL.has(p.status) ? relink(u) : ''}`)
|
|
503
|
+
}
|
|
504
|
+
for (const u of units) {
|
|
505
|
+
if (cleaning.has(String(u.id))) continue
|
|
506
|
+
const off = offLineOf(u)
|
|
507
|
+
if (!u.pr || TERMINAL.has(u.status) || hasWorker(u) || waitsOn(u) !== null || !off.length || !off.every((i) => byId.get(i)?.status === 'succeeded')) continue
|
|
508
|
+
cleaning.add(String(u.id))
|
|
509
|
+
const p = parentOf(u)
|
|
510
|
+
add('restack', u, `${off.join(', ')} landed: rebase unit ${u.id} onto ${p && p.status !== 'succeeded' ? `unit ${p.id}'s HEAD ${p.headSha ? p.headSha.slice(0, 7) : ''}`.trim() : 'the target'} to drop the copies of their commits, ${p && p.status !== 'succeeded' ? `set parentHead to ${p.headSha ? p.headSha.slice(0, 7) : 'that HEAD'}, ` : 'clear stacksOn and parentHead, '}clear offLine, push, and cancel CI on the replaced HEAD; carry a passed review, else the scope script picks the tier`)
|
|
511
|
+
}
|
|
512
|
+
for (const u of units) {
|
|
513
|
+
if (offLineOf(u).length) continue
|
|
514
|
+
const p = parentOf(u)
|
|
515
|
+
if (!u.pr || TERMINAL.has(u.status) || hasWorker(u) || !stale(u) || TERMINAL.has(p.status) || p.reviewedHead !== p.headSha || !p.headSha) continue
|
|
516
|
+
add('restack', u, `unit ${u.id}'s branch is built on ${u.parentHead ? `unit ${p.id}'s old HEAD ${String(u.parentHead).slice(0, 7)}` : `an unrecorded HEAD of unit ${p.id}`}; ${p.id} passed at ${p.headSha.slice(0, 7)}: rebase onto it, set parentHead to ${p.headSha.slice(0, 7)}, push, and cancel CI on the replaced HEAD; carry a passed review, else the scope script picks the tier`)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const whole = chainReady.filter((u) => [...lineOf(u)].every((i) => passed.has(i) && !offLineOf(byId.get(i)).length && !unrecorded(byId.get(i)) && !forked(byId.get(i))) && consistent(u))
|
|
520
|
+
// A passed PR never waits long on an ancestor it only shares files with: move it off one that is in
|
|
521
|
+
// a fix round or blocked at once, and off one still in review after MOVE_OFF_MINUTES.
|
|
522
|
+
const fixing = (v) => { const r = reviews[String(v.id)]; return v.status === 'blocked' || v.status === 'cancelled' || Boolean(r && r.head === v.headSha && (r.fix || (r.complete && r.blocking))) }
|
|
523
|
+
const chainUp = (u) => { const out = []; const seen = new Set([String(u.id)]); let v = u; while (v?.stacksOn?.length) { v = byId.get(String(v.stacksOn[0])); if (!v || seen.has(String(v.id))) break; seen.add(String(v.id)); out.push(v) } return out }
|
|
524
|
+
for (const u of chainReady) {
|
|
525
|
+
if (whole.includes(u) || cleaning.has(String(u.id))) continue
|
|
526
|
+
const id = String(u.id)
|
|
527
|
+
const deps = depsOf(u)
|
|
528
|
+
const held = chainUp(u).filter((v) => v.status !== 'succeeded' && !passed.has(String(v.id)) && !deps.has(String(v.id)))
|
|
529
|
+
if (!held.length) continue
|
|
530
|
+
const r = reviews[id]
|
|
531
|
+
const waited = Number.isFinite(now) && r?.head === u.headSha && Number.isFinite(r.at) ? (now - r.at) / 60_000 : 0
|
|
532
|
+
const stuck = held.filter(fixing)
|
|
533
|
+
if (!stuck.length && waited < MOVE_OFF_MINUTES) continue
|
|
534
|
+
const drop = new Set(held.map((v) => String(v.id)))
|
|
535
|
+
// The new parent is the nearest ancestor whose own line carries none of them; never drop a dependency.
|
|
536
|
+
const up = chainUp(u)
|
|
537
|
+
const at = up.findIndex((v) => ![...lineOf(v)].some((i) => drop.has(i)))
|
|
538
|
+
if (up.slice(0, at < 0 ? up.length : at).some((v) => deps.has(String(v.id)))) continue
|
|
539
|
+
let onto = at < 0 ? null : up[at]
|
|
540
|
+
// In a GitHub stack the dropped ancestor still holds that parent: go to the target, unless that drops a dependency.
|
|
541
|
+
if (onto && taken(onto, u)) { if (up.some((v) => deps.has(String(v.id)))) continue; onto = null }
|
|
542
|
+
const names = held.map((v) => v.id).join(', ')
|
|
543
|
+
if (native && touchesLeaving(u)) continue
|
|
544
|
+
add('restack', u, `unit ${id} passed review but its line waits on ${names}, which it only shares files with (${stuck.length ? `${stuck.map((v) => v.id).join(', ')} is in a fix round or blocked` : `still in review ${Math.floor(waited)}m after this pass`}): move it off: rebase onto ${onto ? `unit ${onto.id}'s branch at ${onto.headSha ? onto.headSha.slice(0, 7) : 'its HEAD'}, set stacksOn to [${onto.id}] and parentHead to that HEAD` : 'the target, clear stacksOn and parentHead'}, dropping their commits (a fix worker resolves conflicts)${native ? `, ${onto ? 'rebase the PR onto that branch' : 'retarget the PR to the target'}` : ''}; its new HEAD takes the tier the scope script picks, its children restack once it passes, and ${names} rebases after it lands${relink(u)}`)
|
|
545
|
+
}
|
|
546
|
+
for (const u of whole) {
|
|
547
|
+
if (whole.some((v) => v !== u && lineOf(v).has(String(u.id)))) continue
|
|
548
|
+
if (native && touchesLeaving(u)) continue
|
|
549
|
+
const line = ordered(lineOf(u))
|
|
550
|
+
const id = String(u.id)
|
|
551
|
+
if (line.length === 1) {
|
|
552
|
+
add('deliver', u, supervised
|
|
553
|
+
? `review passed on ${u.headSha.slice(0, 7)}: when CI is green, hand the PR back merge-ready`
|
|
554
|
+
: `review passed on ${u.headSha.slice(0, 7)}: when CI is green, mark ready and merge (enqueue under a queue); set succeeded when it lands`)
|
|
555
|
+
continue
|
|
556
|
+
}
|
|
557
|
+
add('deliver', u, supervised
|
|
558
|
+
? chain
|
|
559
|
+
? `review passed on every PR in unit ${id}'s line (${line.join(' -> ')}): when CI is green on each HEAD, hand them back merge-ready together; merging ${u.pr} lands the whole line`
|
|
560
|
+
: `review passed on every PR in unit ${id}'s stack (${line.join(' -> ')}): when CI is green on each HEAD, hand the whole stack back merge-ready; queueing ${u.pr} alone queues the stack, and Mergify lands it bottom first`
|
|
561
|
+
: chain
|
|
562
|
+
? `review passed on every PR in unit ${id}'s line (${line.join(' -> ')}): when CI is green on each HEAD, mark them all ready and enqueue only ${u.pr}; its merge lands the line, and GitHub marks the others merged (set each succeeded). While one is not green yet, enqueue the deepest PR whose line is all green, and the rest after`
|
|
563
|
+
: `review passed on every PR in unit ${id}'s stack (${line.join(' -> ')}): when CI is green on each HEAD, mark them all ready and queue only ${u.pr} (@mergifyio queue); the queue takes every PR below it in the GitHub stack, and Mergify lands them bottom first (set each succeeded when it lands). While one is not green yet, queue the deepest PR with every PR below it green, and the rest after: a queue on a red PR still lands the green ones below it`)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// One change per GitHub stack per pass, in this order: a PR leaving it, another restack, a queue, a
|
|
567
|
+
// newly linked child. A later action on a line that overlaps one already changing waits for the next
|
|
568
|
+
// pass's plan: it would rebase onto, queue, or link a HEAD or shape that is about to change. Restacks
|
|
569
|
+
// are settled here; queues and links after the slot cap, against only the restacks that got a slot.
|
|
570
|
+
const rank = (a) => (a.action === 'restack' ? (leaving.has(a.unit) ? 0 : 1) : a.action === 'deliver' ? 2 : a.action === 'open-pr' && openOn.has(a.unit) ? 3 : -1)
|
|
571
|
+
const lineSet = (a) => [...lineOf(byId.get(a.unit)), ...(openOn.has(a.unit) ? lineOf(openOn.get(a.unit)) : [])]
|
|
572
|
+
const oneChange = (list, touched = new Set()) => {
|
|
573
|
+
const drop = new Set()
|
|
574
|
+
for (const a of list.filter((x) => rank(x) >= 0).sort((x, y) => rank(x) - rank(y))) {
|
|
575
|
+
const line = lineSet(a)
|
|
576
|
+
if (line.some((j) => touched.has(j))) drop.add(a)
|
|
577
|
+
else for (const j of line) touched.add(j)
|
|
578
|
+
}
|
|
579
|
+
for (let k = list.length - 1; k >= 0; k--) if (drop.has(list[k])) list.splice(k, 1)
|
|
580
|
+
}
|
|
581
|
+
if (native) {
|
|
582
|
+
const restacks = actions.filter((a) => a.action === 'restack')
|
|
583
|
+
oneChange(restacks)
|
|
584
|
+
for (let k = actions.length - 1; k >= 0; k--) if (actions[k].action === 'restack' && !restacks.includes(actions[k])) actions.splice(k, 1)
|
|
585
|
+
}
|
|
586
|
+
const seenCycles = new Set()
|
|
587
|
+
// A stacksOn loop means no line has a deepest PR; name it like a dependsOn cycle.
|
|
588
|
+
if (linked) {
|
|
589
|
+
for (const u of units) {
|
|
590
|
+
if (TERMINAL.has(u.status)) continue
|
|
591
|
+
const path = [String(u.id)]
|
|
592
|
+
for (let v = parentOf(u); v && !TERMINAL.has(v.status); v = parentOf(v)) {
|
|
593
|
+
const at = path.indexOf(String(v.id))
|
|
594
|
+
if (at >= 0) {
|
|
595
|
+
const c = path.slice(at)
|
|
596
|
+
const key = `s:${[...c].sort().join(',')}`
|
|
597
|
+
if (!seenCycles.has(key)) { seenCycles.add(key); actions.push({ action: 'cycle', unit: c[0], detail: `units ${c.join(' -> ')} -> ${c[0]} are chained on each other, so no line can ship: clear stacksOn and parentHead on ${c[c.length - 1]} and restack it onto the target` }) }
|
|
598
|
+
break
|
|
599
|
+
}
|
|
600
|
+
path.push(String(v.id))
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
for (const u of units) {
|
|
605
|
+
if (TERMINAL.has(u.status)) continue
|
|
606
|
+
const c = cycleFrom(u)
|
|
607
|
+
if (!c || seenCycles.has([...c].sort().join(','))) continue
|
|
608
|
+
seenCycles.add([...c].sort().join(','))
|
|
609
|
+
actions.push({ action: 'cycle', unit: c[0], detail: `units ${c.join(' -> ')} -> ${c[0]} depend on each other, so none can start: remove the link that is not a real dependency (on GitHub and in dependsOn)` })
|
|
610
|
+
}
|
|
611
|
+
const SYNC = ['drop-unit', 'add-unit', 'set-deps', 'move-link', 'blocked-outside', 'cycle']
|
|
612
|
+
for (const a of sync?.actions ?? []) actions.push({ action: a.action, unit: a.unit, detail: a.detail })
|
|
613
|
+
const order = [...SYNC, 'worker-kind', 'decide', 'split', 'unblock', 'check-worker', 'restack', 'carry', 'review-verdict', 'start-fix', 'start-review', 'deliver', 'open-pr', 'resume', 'fill-slot']
|
|
614
|
+
actions.sort((a, b) => order.indexOf(a.action) - order.indexOf(b.action))
|
|
615
|
+
// One restack per unit per pass: the first reason found; the next pass sees the new HEAD.
|
|
616
|
+
const restacked = new Set()
|
|
617
|
+
for (let i = 0; i < actions.length; i++) if (actions[i].action === 'restack') { if (restacked.has(actions[i].unit)) actions.splice(i--, 1); else restacked.add(actions[i].unit) }
|
|
618
|
+
const busy = units.filter(hasWorker).length
|
|
619
|
+
// The Harness already counted our workers along with other work and reservations.
|
|
620
|
+
// Without a capacity source every worker-needing action is due: the harness spawn limit governs.
|
|
621
|
+
const unlimited = effectiveCapacity.source === 'none'
|
|
622
|
+
const slots = unlimited ? null : effectiveCapacity.source === 'harness' ? effectiveCapacity.maxConcurrent : effectiveCapacity.slots
|
|
623
|
+
let free = unlimited ? Infinity : effectiveCapacity.source === 'harness' ? effectiveCapacity.availableSlots : Math.max(0, slots - busy)
|
|
624
|
+
const due = []
|
|
625
|
+
const waiting = []
|
|
626
|
+
for (const a of actions) {
|
|
627
|
+
if (!NEEDS_SLOT.has(a.action)) { due.push(a); continue }
|
|
628
|
+
if (free > 0) { due.push(unlimited ? { ...a, detail: `${a.detail} [needs a free worker slot]` } : a); free-- } else waiting.push(a)
|
|
629
|
+
}
|
|
630
|
+
if (native) oneChange(due)
|
|
631
|
+
const treeChanged = (sync?.actions ?? []).length > 0
|
|
632
|
+
for (const u of units) {
|
|
633
|
+
if (u.status === 'blocked' && hasWorker(u) && !settledBlock(u)) due.push({ action: 'unblock', unit: String(u.id), detail: `unit ${u.id} is blocked without a reason from expire.md's list while worker ${u.worker} is recorded: if that worker is gone, clear it and set the unit in-flight; a block needs a blockedReason` })
|
|
634
|
+
}
|
|
635
|
+
// A block whose cause can clear by itself (CI, a runner, a provider, the network, a dead worker) gets
|
|
636
|
+
// automatic fresh attempts at growing gaps; a due attempt is printed even while other units work.
|
|
637
|
+
const retryable = units.filter((u) => u.status === 'blocked' && AUTO_RETRY.has(u.blockedReason) && (u.autoRetries ?? 0) < AUTO_RETRY_GAPS_MIN.length && !hasWorker(u))
|
|
638
|
+
const dueAt = (u) => (Date.parse(u.blockedAt ?? '') || 0) + AUTO_RETRY_GAPS_MIN[u.autoRetries ?? 0] * 60_000
|
|
639
|
+
// A block with no blockedAt (an older plan) gets its first fresh attempt only once nothing else is left.
|
|
640
|
+
const ready = retryable.filter((u) => u.blockedAt && Number.isFinite(now) && now >= dueAt(u))
|
|
641
|
+
const retryDetail = (u) => (u.blockedReason === 'external-blocker'
|
|
642
|
+
? `blocker unit ${u.id} (a cause outside the run) may have cleared: rerun the command its issue records, unchanged; if it exits 0, close the blocker issue and mark ${u.id} succeeded so the units behind it continue; if not, set autoRetries ${(u.autoRetries ?? 0) + 1} and a new blockedAt`
|
|
643
|
+
: `unit ${u.id} is blocked (${u.blockedReason}) and its cause may have cleared: set it in-flight with autoRetries ${(u.autoRetries ?? 0) + 1} and give it a fresh attempt on the current state; if it fails the same way, block it again with a new blockedAt`)
|
|
644
|
+
for (const u of ready) due.push({ action: 'retry-blocked', unit: String(u.id), detail: retryDetail(u) })
|
|
645
|
+
const parked = units.filter(notStarted).map((u) => `${u.id} (waits on ${waitsOn(u)})`)
|
|
646
|
+
const built = units.filter((u) => builtWaiting(u) && !u.pr).map((u) => `${u.id} (waits on ${waitsOn(u)})`)
|
|
647
|
+
const openWaiting = units.filter((u) => builtWaiting(u) && u.pr).map((u) => `${u.id} (${u.pr}, waits on ${waitsOn(u)})`)
|
|
648
|
+
if (!ready.length && units.every((u) => (TERMINAL.has(u.status) || notStarted(u) || builtWaiting(u)) && settledBlock(u) && !sizeHeld(u) && !deciding(u)) && !treeChanged) {
|
|
649
|
+
if (sync?.fetchError) due.push({ action: 'sync', unit: null, detail: `every unit is settled, but the issue tree could not be read (${sync.fetchError.map((e) => e.error).join('; ')}): rerun with --sync until it reads; never finish on a tree you could not read` })
|
|
650
|
+
else if (sync && !String(sync.source).startsWith('live')) due.push({ action: 'sync', unit: null, detail: 'every unit is settled: rerun with --sync to check the issue tree for new issues before finishing' })
|
|
651
|
+
else if (retryable.some((u) => !u.blockedAt || !Number.isFinite(now))) {
|
|
652
|
+
for (const u of retryable.filter((v) => !v.blockedAt || !Number.isFinite(now))) due.push({ action: 'retry-blocked', unit: String(u.id), detail: retryDetail(u) })
|
|
653
|
+
} else if (retryable.length) {
|
|
654
|
+
const next = retryable.reduce((a, u) => (dueAt(u) < dueAt(a) ? u : a))
|
|
655
|
+
due.push({ action: 'wait', unit: String(next.id), detail: `unit ${next.id} is blocked (${next.blockedReason}); its next automatic attempt is at ${new Date(dueAt(next)).toISOString()}: keep a heartbeat until then and do not finish` })
|
|
656
|
+
} else due.push({ action: 'finish', unit: null, detail: `every unit is delivered, blocked, cancelled, or not started behind one of those: write the handoff${parked.length ? `; not started: ${parked.join(', ')}` : ''}${built.length ? `; built, PR not opened: ${built.join(', ')}` : ''}${openWaiting.length ? `; PR open, cannot land: ${openWaiting.join(', ')}` : ''}` })
|
|
657
|
+
}
|
|
658
|
+
// How long each due action has gone undone, from when it was first printed.
|
|
659
|
+
const nextSeen = {}
|
|
660
|
+
for (const a of due) {
|
|
661
|
+
const key = `${a.action}:${a.unit ?? ''}`
|
|
662
|
+
const first = Number.isFinite(seen[key]) ? seen[key] : now
|
|
663
|
+
if (Number.isFinite(first)) nextSeen[key] = first
|
|
664
|
+
const undone = Number.isFinite(now) && Number.isFinite(first) ? Math.floor((now - first) / 60_000) : 0
|
|
665
|
+
// deliver and check-worker repeat while CI, the queue, or a worker runs: not neglect.
|
|
666
|
+
if (undone >= UNDONE_NOTE_MINUTES && !WAITS_ON_OTHERS.has(a.action)) a.detail += ` [printed ${undone}m ago and still not done: do it now]`
|
|
667
|
+
}
|
|
668
|
+
return { busy, slots, capacity: effectiveCapacity, due, waiting, seen: nextSeen }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// The newest file change in each busy unit's workspace, in ms. Skips dependency and build
|
|
672
|
+
// output (a worker that only downloads is not progressing) and stops after `limit` entries.
|
|
673
|
+
const SKIP_DIRS = new Set(['node_modules', '.gradle', 'build', 'dist', '.next', '.turbo', '.cache', 'Pods'])
|
|
674
|
+
export function readActivity(plan, limit = 20_000) {
|
|
675
|
+
const units = (Array.isArray(plan?.units) ? plan.units : plan?.issues) ?? []
|
|
676
|
+
const out = {}
|
|
677
|
+
for (const u of units) {
|
|
678
|
+
if (!hasWorker(u) || typeof u.workspace !== 'string' || !existsSync(u.workspace)) continue
|
|
679
|
+
let newest = 0
|
|
680
|
+
let seen = 0
|
|
681
|
+
const stack = [u.workspace]
|
|
682
|
+
for (const f of ['.git/index', '.git/logs/HEAD']) {
|
|
683
|
+
try { newest = Math.max(newest, statSync(join(u.workspace, f)).mtimeMs) } catch { /* not a git checkout */ }
|
|
684
|
+
}
|
|
685
|
+
while (stack.length && seen < limit) {
|
|
686
|
+
const dir = stack.pop()
|
|
687
|
+
let entries = []
|
|
688
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { continue }
|
|
689
|
+
for (const e of entries) {
|
|
690
|
+
if (++seen > limit) break
|
|
691
|
+
if (e.isDirectory()) { if (e.name !== '.git' && !SKIP_DIRS.has(e.name)) stack.push(join(dir, e.name)); continue }
|
|
692
|
+
try { newest = Math.max(newest, statSync(join(dir, e.name)).mtimeMs) } catch { /* removed meanwhile */ }
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (newest) out[String(u.id)] = newest
|
|
696
|
+
}
|
|
697
|
+
return out
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// The newest review round per unit, from <run>/reviews/<unit>-r<N>.json.
|
|
701
|
+
export function readReviews(runDir) {
|
|
702
|
+
const dir = join(runDir, 'reviews')
|
|
703
|
+
const out = {}
|
|
704
|
+
if (!existsSync(dir)) return out
|
|
705
|
+
for (const name of readdirSync(dir)) {
|
|
706
|
+
const m = name.match(/^(.+)-r(\d+)\.json$/)
|
|
707
|
+
if (!m) continue
|
|
708
|
+
const [, unit, n] = m
|
|
709
|
+
const round = Number(n)
|
|
710
|
+
if (out[unit] && out[unit].round >= round) continue
|
|
711
|
+
let saved = {}
|
|
712
|
+
try { saved = JSON.parse(readFileSync(join(dir, name), 'utf8')) ?? {} } catch { saved = {} }
|
|
713
|
+
const awaiting = Array.isArray(saved.external?.awaiting) && saved.external.awaiting.length > 0
|
|
714
|
+
const blocking = Array.isArray(saved.findings) && saved.findings.some((f) => BLOCKING.includes(f?.severity))
|
|
715
|
+
const decidePath = join(dir, `${unit}-r${round}.decide.json`)
|
|
716
|
+
const decide = existsSync(decidePath)
|
|
717
|
+
let ask = ''
|
|
718
|
+
if (decide) {
|
|
719
|
+
try { ask = (JSON.parse(readFileSync(decidePath, 'utf8'))?.question ?? []).map((q) => `${q.id}: ${q.summary}`).join('; ') } catch { ask = '' }
|
|
720
|
+
}
|
|
721
|
+
let at
|
|
722
|
+
try { at = statSync(join(dir, name)).mtimeMs } catch { at = undefined }
|
|
723
|
+
out[unit] = { round, at, head: saved.head ?? null, fix: existsSync(join(dir, `${unit}-r${round}.fix.json`)), complete: saved.complete === true, awaiting, blocking, ...(decide ? { decide, ask } : {}) }
|
|
724
|
+
}
|
|
725
|
+
return out
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Each unit's open worker questions, from <run>/results/*.json: a result whose
|
|
729
|
+
// `decisionNeeded` is set (a string, or an object with `question`). Its unit is the result's
|
|
730
|
+
// `unit`, else the file name up to its first '-'.
|
|
731
|
+
export function readQuestions(runDir) {
|
|
732
|
+
const dir = join(runDir, 'results')
|
|
733
|
+
const out = {}
|
|
734
|
+
if (!existsSync(dir)) return out
|
|
735
|
+
for (const name of readdirSync(dir).sort()) {
|
|
736
|
+
if (!name.endsWith('.json')) continue
|
|
737
|
+
let saved
|
|
738
|
+
try { saved = JSON.parse(readFileSync(join(dir, name), 'utf8')) } catch { continue }
|
|
739
|
+
const need = saved?.decisionNeeded
|
|
740
|
+
const ask = typeof need === 'string' ? need.trim() : need && typeof need === 'object' ? String(need.question ?? JSON.stringify(need)).trim() : ''
|
|
741
|
+
if (!ask) continue
|
|
742
|
+
const unit = String(saved.unit ?? name.split('-')[0].replace(/\.json$/, ''))
|
|
743
|
+
;(out[unit] ??= []).push({ key: `results/${name}`, ask })
|
|
744
|
+
}
|
|
745
|
+
return out
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const USAGE = 'usage: next-actions.mjs --run <run dir> [--capacity-json <fresh snapshot JSON> | --slots <explicit workflow budget>] [--quiet-minutes 45] [--resume] [--sync] [--json] [--peek] [--mode full|narrow|off] [--rules] [--exit-due]\nexit 0 when it ran (the outcome is in the output; --json carries exitDue), 2 on error; --exit-due exits 1 when anything is due. Without a capacity flag every worker action is due, noted as needing a free slot.'
|
|
749
|
+
|
|
750
|
+
export function parseCliArgs(argv) {
|
|
751
|
+
const opts = { quietMinutes: DEFAULT_QUIET_MINUTES, json: false, resume: false, sync: false, peek: false }
|
|
752
|
+
for (let i = 0; i < argv.length; i++) {
|
|
753
|
+
const flag = argv[i]
|
|
754
|
+
if (flag === '--json') { opts.json = true; continue }
|
|
755
|
+
if (flag === '--resume') { opts.resume = true; continue }
|
|
756
|
+
if (flag === '--sync') { opts.sync = true; continue }
|
|
757
|
+
if (flag === '--peek') { opts.peek = true; continue }
|
|
758
|
+
if (flag === '--rules') { opts.rules = true; continue }
|
|
759
|
+
if (flag === '--exit-due') { opts.exitDue = true; continue }
|
|
760
|
+
if (flag === '--mode' && argv[i + 1] !== undefined) { opts.mode = argv[++i]; continue }
|
|
761
|
+
if (flag === '--quiet-minutes' && argv[i + 1] !== undefined) { opts.quietMinutes = Number(argv[++i]); continue }
|
|
762
|
+
if (!['--run', '--slots', '--capacity-json'].includes(flag) || argv[i + 1] === undefined) throw new NextActionsError(USAGE)
|
|
763
|
+
const key = flag === '--capacity-json' ? 'capacityJson' : flag.slice(2)
|
|
764
|
+
if (opts[key] !== undefined) throw new NextActionsError(`${flag} may be supplied only once`)
|
|
765
|
+
opts[key] = argv[++i]
|
|
766
|
+
}
|
|
767
|
+
if (!opts.run) throw new NextActionsError(USAGE)
|
|
768
|
+
if (opts.slots !== undefined && opts.capacityJson !== undefined) {
|
|
769
|
+
throw new NextActionsError('supply at most one capacity source: --capacity-json or --slots')
|
|
770
|
+
}
|
|
771
|
+
if (opts.capacityJson !== undefined) {
|
|
772
|
+
let parsed
|
|
773
|
+
try { parsed = JSON.parse(opts.capacityJson) } catch { throw new NextActionsError('--capacity-json must be valid JSON') }
|
|
774
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new NextActionsError('--capacity-json must contain a Harness snapshot object')
|
|
775
|
+
opts.capacity = normalizeCapacity(parsed)
|
|
776
|
+
delete opts.capacityJson
|
|
777
|
+
} else if (opts.slots !== undefined) {
|
|
778
|
+
if (!/^\d+$/.test(opts.slots)) throw new NextActionsError('--slots must be a nonnegative safe integer')
|
|
779
|
+
opts.slots = Number(opts.slots)
|
|
780
|
+
normalizeCapacity(opts.slots)
|
|
781
|
+
}
|
|
782
|
+
if (!(opts.quietMinutes > 0) || !Number.isFinite(opts.quietMinutes)) throw new NextActionsError('--quiet-minutes must be a positive finite number')
|
|
783
|
+
return opts
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (isMainModule(import.meta.url)) {
|
|
787
|
+
try {
|
|
788
|
+
const opts = parseCliArgs(process.argv.slice(2))
|
|
789
|
+
let plan
|
|
790
|
+
try { plan = JSON.parse(readFileSync(join(opts.run, 'task-plan.json'), 'utf8')) } catch (error) {
|
|
791
|
+
throw new NextActionsError(`cannot read ${join(opts.run, 'task-plan.json')}: ${error.message}`)
|
|
792
|
+
}
|
|
793
|
+
// A plan without `skill` reads as resolve-issues: a rush run would defer a size hold and
|
|
794
|
+
// rehydrate would re-read the wrong SKILL.md, so refuse it rather than guess.
|
|
795
|
+
if (!RUN_SKILLS.includes(plan?.skill)) {
|
|
796
|
+
throw new NextActionsError(`task-plan.json needs "skill": "resolve-issues" or "rush-issues" (the skill this run follows); found ${JSON.stringify(plan?.skill)}`)
|
|
797
|
+
}
|
|
798
|
+
let sync = null
|
|
799
|
+
try {
|
|
800
|
+
sync = syncRun(opts.run, { live: opts.resume || opts.sync })
|
|
801
|
+
} catch (error) {
|
|
802
|
+
sync = { synced: false, actions: [], fetchError: [{ key: 'plan', error: error.message }] }
|
|
803
|
+
}
|
|
804
|
+
const seenPath = join(opts.run, 'next-actions-seen.json')
|
|
805
|
+
let seen = {}
|
|
806
|
+
try { seen = JSON.parse(readFileSync(seenPath, 'utf8')) ?? {} } catch { seen = {} }
|
|
807
|
+
const result = nextActions(plan, readReviews(opts.run), opts.capacity ?? opts.slots, { resume: opts.resume, now: Date.now(), activity: readActivity(plan), quietMinutes: opts.quietMinutes, sync: sync.source === 'none' && !sync.missingTree && !sync.fetchError ? null : sync, seen, questions: readQuestions(opts.run) })
|
|
808
|
+
if (!opts.peek) {
|
|
809
|
+
try { writeFileSync(seenPath, `${JSON.stringify(result.seen)}\n`) } catch { /* a read-only run dir only loses the undone note */ }
|
|
810
|
+
}
|
|
811
|
+
delete result.seen
|
|
812
|
+
// full: every due step; narrow: done or not, units with work, guards; off: done or not only.
|
|
813
|
+
const mode = opts.mode ?? plan.schedulerMode ?? 'full'
|
|
814
|
+
if (!SCHEDULER_MODES.includes(mode)) throw new NextActionsError(`scheduler mode must be ${SCHEDULER_MODES.join(', ')}: ${JSON.stringify(mode)}`)
|
|
815
|
+
const exitDue = result.due.length > 0
|
|
816
|
+
const showRules = opts.resume || opts.rules
|
|
817
|
+
const budgetLine = (capacity) => capacity.source === 'harness'
|
|
818
|
+
? `workers ${result.busy}; Harness ${capacity.occupiedSlots}/${capacity.maxConcurrent} occupied, ${capacity.availableSlots} available`
|
|
819
|
+
: capacity.source === 'none'
|
|
820
|
+
? `workers ${result.busy}; no capacity given: worker actions need a free worker slot (the harness spawn limit governs)`
|
|
821
|
+
: `slots ${result.busy}/${result.slots} busy (explicit workflow budget)`
|
|
822
|
+
const rulesLine = (text) => showRules ? `${'rules'.padEnd(15)}${text} Full text: ${SKILL_PATH}\n` : `${'rules'.padEnd(15)}Rules: ${SKILL_PATH} (--rules prints them)\n`
|
|
823
|
+
if (opts.json) process.stdout.write(`${JSON.stringify({ ...result, mode, exitDue }, null, 2)}\n`)
|
|
824
|
+
else if (mode !== 'full') {
|
|
825
|
+
const view = narrowView(plan, result)
|
|
826
|
+
const guardNames = [...new Set(result.due.filter((a) => /^guards: (?!none)/m.test(view) && ['decide', 'review-verdict', 'unblock', 'worker-kind', 'split', 'sync', 'add-unit', 'drop-unit', 'set-deps', 'move-link', 'blocked-outside'].includes(a.action)).map((a) => a.action))]
|
|
827
|
+
process.stdout.write(`${mode === 'off' ? `${view.split('\n')[0].replace(/: .*$/, '')}${guardNames.length ? ` (clear first: ${guardNames.join(', ')})` : ''}` : view}\n`)
|
|
828
|
+
process.stdout.write(`${budgetLine(result.capacity)}\n`)
|
|
829
|
+
process.stdout.write(rulesLine(`${mode === 'off' ? 'Scheduler mode off: plan every step yourself from the plan and results.' : 'Scheduler mode narrow: clear the guards, then choose each unit\'s next step yourself.'} The run ends only when this prints "run done".`))
|
|
830
|
+
} else {
|
|
831
|
+
for (const a of result.due) process.stdout.write(`${a.action.padEnd(15)}${a.detail}\n`)
|
|
832
|
+
for (const a of result.waiting) process.stdout.write(`${'waiting-slot'.padEnd(15)}${a.detail}\n`)
|
|
833
|
+
process.stdout.write(`${budgetLine(result.capacity)}; ${result.due.length} due\n`)
|
|
834
|
+
process.stdout.write(rulesLine(RULES))
|
|
835
|
+
}
|
|
836
|
+
process.exitCode = opts.exitDue && exitDue ? 1 : 0
|
|
837
|
+
} catch (error) {
|
|
838
|
+
process.stderr.write(`next-actions: ${error.message}\n`)
|
|
839
|
+
process.exitCode = 2
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// The narrow view: whether the run is done, which units have work, the guards, and what waits on
|
|
844
|
+
// the human. It leaves each unit's next step to the coordinator.
|
|
845
|
+
export function narrowView(plan, result) {
|
|
846
|
+
const GUARDS = new Set(['decide', 'review-verdict', 'unblock', 'worker-kind', 'split', 'sync', 'add-unit', 'drop-unit', 'set-deps', 'move-link', 'blocked-outside'])
|
|
847
|
+
const units = new Map((plan.units ?? []).map((u) => [String(u.id), u]))
|
|
848
|
+
const guards = result.due.filter((a) => GUARDS.has(a.action)).map((a) => `${a.unit ?? 'run'}: ${a.detail}`)
|
|
849
|
+
const work = [...new Set([...result.due, ...result.waiting].filter((a) => a.unit && !GUARDS.has(a.action) && a.action !== 'wait').map((a) => a.unit))]
|
|
850
|
+
const wait = result.due.find((a) => a.action === 'wait')
|
|
851
|
+
const held = [...units.values()].filter((u) => u.status === 'blocked' && u.blockedReason === 'human-decision').map((u) => `${u.id} (item ${u.humanItem ?? '?'}: ${u.humanNeed ?? 'unstated'})`)
|
|
852
|
+
const done = result.due.some((a) => a.action === 'finish')
|
|
853
|
+
const state = done ? 'done: write the handoff' : guards.length ? 'not done: clear the guards first' : work.length ? 'not done: units have work' : wait ? `not done: waiting (${wait.detail})` : 'not done: workers are running'
|
|
854
|
+
return [`run ${state}`, `units with work: ${work.join(', ') || 'none'} (choose each unit's next step yourself)`,
|
|
855
|
+
`guards: ${guards.length ? guards.join(' | ') : 'none'}`, `held on the human: ${held.join(', ') || 'none'}`].join('\n')
|
|
856
|
+
}
|