session-orchestrator 5.11.0 → 5.13.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/.claude-plugin/marketplace.json +12 -1
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +180 -3
- package/README.md +126 -117
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +4 -2
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +102 -25
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +55 -21
- package/docs/session-config-template.md +32 -24
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +158 -59
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -3
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +73 -3
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +3 -3
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +21 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-end/vault-orphans.mjs +205 -0
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-invocation.mjs +14 -3
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +85 -12
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +174 -0
- package/scripts/lib/so-hud/runtime.mjs +87 -0
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +248 -0
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md/recommendations.mjs +26 -4
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +22 -9
- package/scripts/lib/vault-mirror/process.mjs +197 -65
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +106 -13
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +34 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +57 -8
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +68 -0
- package/scripts/state-md-idle-reset.mjs +82 -0
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/scripts/vault-orphans.mjs +171 -0
- package/skills/_shared/fleet-protocol.md +166 -3
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +5 -4
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/autopilot/SKILL.md +3 -2
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +97 -13
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +8 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +10 -4
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-7-5-mode-selector.md +3 -1
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/AGENTS.md +0 -207
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Local partial pre-push gate (#1642): the route the hook takes when no remote host
|
|
3
|
+
// could run the full gate (router exit 10) AND the push goes to an origin MR branch.
|
|
4
|
+
// Exit 0 verified, 1 blocked, 10 not determinable (the hook then runs the full gate
|
|
5
|
+
// locally — fail-closed, same contract as LSO scripts/pre-push-gate.sh, Owner #801).
|
|
6
|
+
//
|
|
7
|
+
// It checks what this push changes since the merge-base with origin's default branch:
|
|
8
|
+
// `node --check` + ESLint on the changed source files and `vitest related` for them.
|
|
9
|
+
// The full suite runs in the MR pipeline on the pushed head SHA.
|
|
10
|
+
//
|
|
11
|
+
// CEILING (BV-004): `vitest related` follows the static import graph. A test that
|
|
12
|
+
// reaches a changed file only by spawning it (hooks, .husky/, CLIs run via
|
|
13
|
+
// spawnSync) or by reading it from disk (policy JSON, Markdown, fixtures — edited OR
|
|
14
|
+
// deleted) is not selected here; the MR pipeline is the net for that class. Only a
|
|
15
|
+
// deleted or renamed SOURCE file falls back to the full gate, because its importers
|
|
16
|
+
// break while nothing is left to select (#1643: falling back on every deletion would
|
|
17
|
+
// send each archive move of a doc to the full local suite).
|
|
18
|
+
// Revisit trigger: an MR pipeline red that a selected-by-path test would have caught
|
|
19
|
+
// locally on a push routed through this gate.
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
24
|
+
|
|
25
|
+
const log = (message) => process.stderr.write(`pre-push: ${message}\n`);
|
|
26
|
+
|
|
27
|
+
/** Changed files that make a selection meaningless: they reconfigure every test or lint run. */
|
|
28
|
+
const FULL_SUITE_TRIGGERS = [/^package(-lock)?\.json$/, /^vitest\.config\.[cm]?[jt]s$/, /^eslint\.config\.[cm]?[jt]s$/];
|
|
29
|
+
const SOURCE_RE = /\.(mjs|cjs|js)$/;
|
|
30
|
+
|
|
31
|
+
function git(repoRoot, args) {
|
|
32
|
+
// core.quotePath=false: git otherwise prints a non-ASCII path as "\303\244.mjs" (quoted,
|
|
33
|
+
// escaped), and every suffix test below would miss it (#1643).
|
|
34
|
+
const r = spawnSync('git', ['-C', repoRoot, '-c', 'core.quotePath=false', ...args], { encoding: 'utf8', timeout: 30_000 });
|
|
35
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Merge-base of the pushed SHA with origin's default branch, or null when no
|
|
40
|
+
* origin default ref resolves in the source repo (the materialised clone has none).
|
|
41
|
+
*/
|
|
42
|
+
export function resolveBase(repoRoot, sha) {
|
|
43
|
+
const head = git(repoRoot, ['symbolic-ref', '-q', '--short', 'refs/remotes/origin/HEAD']);
|
|
44
|
+
for (const ref of [head, 'origin/main'].filter(Boolean)) {
|
|
45
|
+
if (git(repoRoot, ['rev-parse', '-q', '--verify', `${ref}^{commit}`]) === null) continue;
|
|
46
|
+
return git(repoRoot, ['merge-base', sha, ref]) || null;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Runs one tool from the tree's own node_modules; null when the tool is not installed there. */
|
|
52
|
+
function runTool(tree, bin, args, timeout) {
|
|
53
|
+
const path = join(tree, 'node_modules/.bin', bin);
|
|
54
|
+
if (!existsSync(path)) return null;
|
|
55
|
+
const r = spawnSync(process.execPath, [path, ...args], { cwd: tree, stdio: ['ignore', 2, 2], timeout });
|
|
56
|
+
return r.status === 0 && !r.error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function emit(ledgerRoot, passed, fields) {
|
|
60
|
+
try {
|
|
61
|
+
const { emitEvent, sessionAttribution } = await import('./events.mjs');
|
|
62
|
+
await emitEvent(`orchestrator.quality_gate.${passed ? 'passed' : 'failed'}`, {
|
|
63
|
+
variant: 'local-partial', exit_code: passed ? 0 : 1, host: 'local', host_source: 'fallback',
|
|
64
|
+
...fields, ...sessionAttribution(ledgerRoot),
|
|
65
|
+
}, { repoRoot: ledgerRoot });
|
|
66
|
+
} catch { /* telemetry cannot change the blocking verdict */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
70
|
+
const [tree, sha, repoRoot] = argv;
|
|
71
|
+
if (!tree || !sha || !repoRoot) return 10;
|
|
72
|
+
const base = resolveBase(repoRoot, sha);
|
|
73
|
+
if (!base) {
|
|
74
|
+
log('local partial gate: no merge-base with origin\'s default branch — full gate locally (fail-closed).');
|
|
75
|
+
return 10;
|
|
76
|
+
}
|
|
77
|
+
// --no-renames: a rename reads as delete + add, so the old path stays visible.
|
|
78
|
+
const diff = git(repoRoot, ['diff', '--name-only', '--no-renames', '--diff-filter=d', base, sha]);
|
|
79
|
+
const deleted = git(repoRoot, ['diff', '--name-only', '--no-renames', '--diff-filter=D', base, sha]);
|
|
80
|
+
if (diff === null || deleted === null) return 10;
|
|
81
|
+
// A deleted or renamed module breaks its importers, and `vitest related` cannot
|
|
82
|
+
// select tests for a file that no longer exists.
|
|
83
|
+
const gone = deleted.split('\n').find((f) => SOURCE_RE.test(f));
|
|
84
|
+
if (gone) {
|
|
85
|
+
log(`local partial gate: ${gone} deleted or renamed — full gate locally (fail-closed).`);
|
|
86
|
+
return 10;
|
|
87
|
+
}
|
|
88
|
+
const changed = diff.split('\n').filter(Boolean);
|
|
89
|
+
const trigger = changed.find((f) => FULL_SUITE_TRIGGERS.some((re) => re.test(f)));
|
|
90
|
+
if (trigger) {
|
|
91
|
+
log(`local partial gate: ${trigger} changed — full gate locally (fail-closed).`);
|
|
92
|
+
return 10;
|
|
93
|
+
}
|
|
94
|
+
const sources = changed.filter((f) => SOURCE_RE.test(f) && existsSync(join(tree, f)));
|
|
95
|
+
log(`local partial gate: ${changed.length} changed file(s) since ${base.slice(0, 12)}; the full suite runs in the MR pipeline on ${sha.slice(0, 12)} (#1642).`);
|
|
96
|
+
const fields = { pushed_sha: sha, base_sha: base, changed_count: changed.length };
|
|
97
|
+
if (changed.length === 0) {
|
|
98
|
+
await emit(repoRoot, true, fields);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
const syntaxFailed = sources.filter((f) =>
|
|
102
|
+
spawnSync(process.execPath, ['--check', f], { cwd: tree, stdio: ['ignore', 2, 2], timeout: 30_000 }).status !== 0);
|
|
103
|
+
if (syntaxFailed.length) log(`local partial gate: node --check failed: ${syntaxFailed.join(', ')}`);
|
|
104
|
+
const lint = sources.length ? runTool(tree, 'eslint', ['--no-warn-ignored', ...sources], 300_000) : true;
|
|
105
|
+
const test = runTool(tree, 'vitest', ['related', '--run', '--passWithNoTests', ...changed], 900_000);
|
|
106
|
+
if (lint === null || test === null) {
|
|
107
|
+
log('local partial gate: eslint or vitest missing in node_modules — full gate locally (fail-closed).');
|
|
108
|
+
return 10;
|
|
109
|
+
}
|
|
110
|
+
const passed = syntaxFailed.length === 0 && lint && test;
|
|
111
|
+
const failedChecks = [syntaxFailed.length && 'typecheck', !lint && 'lint', !test && 'test'].filter(Boolean);
|
|
112
|
+
await emit(repoRoot, passed, { ...fields, ...(failedChecks.length ? { failed_checks: failedChecks } : {}) });
|
|
113
|
+
log(`local partial gate ${passed ? 'passed' : 'FAILED — Push blocked'}.`);
|
|
114
|
+
return passed ? 0 : 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isMainModule(import.meta.url)) process.exitCode = await main();
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Optional pre-push router: 0 verified, 1 blocked,
|
|
2
|
+
// Optional pre-push router: 0 verified, 1 blocked, 2 SO_GATE_HOSTS invalid or none of its
|
|
3
|
+
// hosts could run the gate (blocked, no local fallback), 10 transport/readiness fallback.
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { statSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
5
9
|
import { readConfigFile } from './config/io.mjs';
|
|
6
10
|
import { _parseRemoteHosts } from './config/remote-hosts.mjs';
|
|
7
11
|
import { isMainModule } from './is-main-module.mjs';
|
|
@@ -44,64 +48,186 @@ function remoteCommand(sha, nonce) {
|
|
|
44
48
|
`;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
const ALIAS_SHAPE = /^[A-Za-z0-9._-]+$/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the `SO_GATE_HOSTS` override (#1626) against the offload host registry.
|
|
55
|
+
*
|
|
56
|
+
* The registry rule mirrors offload's own `load_host`:
|
|
57
|
+
* `${OFFLOAD_CONFIG_DIR:-$HOME/.config/remote-offload}/hosts/<alias>.env` must be a
|
|
58
|
+
* regular file. The alias shape is checked BEFORE any path is built, so `/` and
|
|
59
|
+
* `..` can never reach the filesystem.
|
|
60
|
+
*
|
|
61
|
+
* @param {NodeJS.ProcessEnv} env
|
|
62
|
+
* @returns {{ hosts: string[], source: 'override' } | { error: string } | null}
|
|
63
|
+
* null when the override is unset (whitespace-only counts as unset).
|
|
64
|
+
*/
|
|
65
|
+
export function resolveGateHostOverride(env = process.env) {
|
|
66
|
+
const raw = (env.SO_GATE_HOSTS || '').trim();
|
|
67
|
+
if (!raw) return null;
|
|
68
|
+
const configDir = (env.OFFLOAD_CONFIG_DIR || '').trim() || join((env.HOME || '').trim() || homedir(), '.config/remote-offload');
|
|
69
|
+
const registry = join(configDir, 'hosts');
|
|
70
|
+
const hosts = raw.split(',').map((alias) => alias.trim()).filter(Boolean);
|
|
71
|
+
for (const alias of hosts) {
|
|
72
|
+
let known = false;
|
|
73
|
+
if (ALIAS_SHAPE.test(alias) && alias !== '.' && alias !== '..') {
|
|
74
|
+
try { known = statSync(join(registry, `${alias}.env`)).isFile(); } catch { known = false; }
|
|
75
|
+
}
|
|
76
|
+
if (!known) {
|
|
77
|
+
return { error: `SO_GATE_HOSTS names unknown or invalid host '${alias}' — no regular file ${alias}.env in ${registry}. Push blocked (no local fallback).` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return hosts.length ? { hosts, source: 'override' } : { error: `SO_GATE_HOSTS='${raw}' contains no host alias. Push blocked.` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The `gate-ready=<value>` token of an `offload doctor --brief` run (stdout or stderr), or null. */
|
|
84
|
+
function gateReadyToken(doctor) {
|
|
85
|
+
return /\bgate-ready=(\S+)/.exec(`${doctor.stdout ?? ''}\n${doctor.stderr ?? ''}`)?.[1] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gate readiness of one host (#1627). doctor's exit code also covers headless
|
|
90
|
+
* readiness, which a gate does not need: a Windows host reports `gate-ready=yes` with
|
|
91
|
+
* exit 1 because `headless-ready=NO`. The token decides when present; an offload
|
|
92
|
+
* without it keeps the old exit-code rule. A spawn error (timeout) is not ready.
|
|
93
|
+
*
|
|
94
|
+
* @param {{ error?: Error, status: number | null, stdout?: string, stderr?: string }} doctor
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
export function gateReady(doctor) {
|
|
98
|
+
if (doctor.error) return false;
|
|
99
|
+
const token = gateReadyToken(doctor);
|
|
100
|
+
return token === null ? doctor.status === 0 : token === 'yes';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Did the remote gate re-run its timeout-only failures clean (#1673)? Checked
|
|
105
|
+
* here on the report's own fields, not on `rerun.status` alone: zero failures,
|
|
106
|
+
* no timeout, no signal sent, no survivor, and exactly the first run's failing
|
|
107
|
+
* files — no more, no fewer.
|
|
108
|
+
*/
|
|
109
|
+
function cleanRerun(test) {
|
|
110
|
+
const rerun = test?.rerun;
|
|
111
|
+
if (rerun?.status !== 'pass' || rerun.failed !== 0 || rerun.timed_out !== false) return false;
|
|
112
|
+
if (!Array.isArray(rerun.kill_signals) || rerun.kill_signals.length !== 0) return false;
|
|
113
|
+
if (!Array.isArray(rerun.survivors) || rerun.survivors.length !== 0) return false;
|
|
114
|
+
const failedFiles = Array.isArray(test.failed_files) ? test.failed_files : [];
|
|
115
|
+
const files = Array.isArray(rerun.files) ? rerun.files : [];
|
|
116
|
+
return failedFiles.length > 0 && files.length === failedFiles.length &&
|
|
117
|
+
new Set(files).size === files.length && failedFiles.every((f) => files.includes(f));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run the full quality gate for `sha` on a remote host via offload (the body of the
|
|
122
|
+
* pre-push router, #1712). Exit-code contract of `code`: 0 verified, 1 blocked,
|
|
123
|
+
* 2 SO_GATE_HOSTS invalid or none of its hosts could run the gate, 10 fallback.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} opts
|
|
126
|
+
* @param {string} opts.tree checkout offload syncs (its basename names the host repo)
|
|
127
|
+
* @param {string} opts.sha commit the remote gate must check out and verify
|
|
128
|
+
* @param {string} opts.ledgerRoot repo root for events and wave attribution
|
|
129
|
+
* @param {NodeJS.ProcessEnv} [opts.env] source of the SO_GATE_HOSTS override
|
|
130
|
+
* @param {boolean} [opts.emit=true] emit `orchestrator.quality_gate.*` — false for
|
|
131
|
+
* callers whose `sha` is not a pushed commit (snapshot probe)
|
|
132
|
+
* @param {string} [opts.jobPrefix='pre-push'] offload job name prefix
|
|
133
|
+
* @param {(message: string) => void} [opts.log]
|
|
134
|
+
* @param {{ write: (chunk: string) => unknown }} [opts.stdout=process.stdout] sink for the remote
|
|
135
|
+
* run's stdout — a caller that owns its own stdout (snapshot probe `--json`) routes it elsewhere
|
|
136
|
+
* @param {{ write: (chunk: string) => unknown }} [opts.stderr=process.stderr] sink for the remote run's stderr
|
|
137
|
+
* @returns {Promise<{ code: number, passed: boolean, alias: string | null, report: object | null, hostSource: string }>}
|
|
138
|
+
*/
|
|
139
|
+
export async function runRemoteGate({ tree, sha, ledgerRoot, env = process.env, emit = true, jobPrefix = 'pre-push', log: logFn = log,
|
|
140
|
+
stdout = process.stdout, stderr = process.stderr }) {
|
|
49
141
|
let hosts;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
142
|
+
let hostSource = 'config';
|
|
143
|
+
const done = (code) => ({ code, passed: false, alias: null, report: null, hostSource });
|
|
144
|
+
const override = resolveGateHostOverride(env);
|
|
145
|
+
if (override?.error) {
|
|
146
|
+
logFn(override.error);
|
|
147
|
+
return done(2);
|
|
148
|
+
}
|
|
149
|
+
if (override) {
|
|
150
|
+
hosts = override.hosts.map((alias) => ({ alias }));
|
|
151
|
+
hostSource = 'override';
|
|
152
|
+
logFn(`gate host order from SO_GATE_HOSTS (host_source: override): ${override.hosts.join(', ')}.`);
|
|
153
|
+
} else {
|
|
154
|
+
try {
|
|
155
|
+
hosts = _parseRemoteHosts(await readConfigFile(tree)).filter((host) => host['roles-allowed'].includes('test'));
|
|
156
|
+
} catch {
|
|
157
|
+
logFn('remote configuration unavailable — using local gate (host_source: fallback).');
|
|
158
|
+
return done(10);
|
|
159
|
+
}
|
|
55
160
|
}
|
|
56
161
|
for (const { alias } of hosts) {
|
|
57
|
-
|
|
162
|
+
logFn(`checking remote readiness (${alias}).`);
|
|
58
163
|
const doctor = spawnSync('offload', ['doctor', '-H', alias, '--brief', '--no-probe'],
|
|
59
164
|
{ encoding: 'utf8', timeout: 30_000, maxBuffer: 1024 * 1024 });
|
|
60
165
|
if (doctor.error?.code === 'ENOENT') break;
|
|
61
|
-
if (doctor
|
|
62
|
-
|
|
166
|
+
if (!gateReady(doctor)) {
|
|
167
|
+
const token = gateReadyToken(doctor);
|
|
168
|
+
logFn(`remote ${alias} not ready (exit ${doctor.status ?? doctor.error?.code}${token ? `, gate-ready=${token}` : ''}) — trying next route.`);
|
|
63
169
|
continue;
|
|
64
170
|
}
|
|
65
171
|
const nonce = randomUUID();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
172
|
+
logFn(`running full quality gate remotely on ${alias} (host_source: ${hostSource}) against pushed SHA ${sha}.`);
|
|
173
|
+
// No --link-modules: offload snapshots a linked node_modules symlink into its commit (.gitignore's
|
|
174
|
+
// `node_modules/` matches directories only), and the `git checkout <sha>` in remoteCommand then
|
|
175
|
+
// deletes it, so vitest/eslint exit 127 and the gate reports them as "fail" (push blocked).
|
|
176
|
+
const run = spawnSync('offload', ['run', '-H', alias, tree, '--job', `${jobPrefix}-${nonce}`,
|
|
177
|
+
'--klasse', 'gate', '--rm', '--timeout', '1800', '--', 'node', '-e', remoteCommand(sha, nonce)],
|
|
69
178
|
{ encoding: 'utf8', timeout: 1_860_000, maxBuffer: 64 * 1024 * 1024 });
|
|
70
|
-
|
|
71
|
-
|
|
179
|
+
stdout.write(run.stdout ?? '');
|
|
180
|
+
stderr.write(run.stderr ?? '');
|
|
72
181
|
const receipts = (run.stdout ?? '').split(/\r?\n/).filter((line) => line.startsWith('SO_PRE_PUSH_RESULT='));
|
|
73
182
|
let receipt;
|
|
74
183
|
try { if (receipts.length === 1) receipt = JSON.parse(receipts[0].slice('SO_PRE_PUSH_RESULT='.length)); } catch { /* unknown blocks */ }
|
|
75
184
|
if (receipts.length === 0 && !run.error && [2, 4, 8, 10, 255].includes(run.status)) {
|
|
76
|
-
|
|
185
|
+
logFn(`remote ${alias} transport/readiness failure (exit ${run.status}) — trying next route.`);
|
|
77
186
|
continue;
|
|
78
187
|
}
|
|
79
188
|
const report = receipt?.report;
|
|
80
189
|
const stubbedChecks = Object.keys(report?.stubbed ?? {});
|
|
81
|
-
if (stubbedChecks.length > 0)
|
|
190
|
+
if (stubbedChecks.length > 0) logFn(`remote gate contains stubbed checks: ${stubbedChecks.join(', ')} — Push blocked.`);
|
|
82
191
|
const passed = !run.error && run.status === 0 && receipt?.nonce === nonce && receipt.sha === sha &&
|
|
83
192
|
receipt.exit_code === 0 && report?.variant === 'full-gate' && report.test?.status === 'pass' &&
|
|
84
|
-
|
|
193
|
+
// `failed` stays the first, parallel run's count; it is acceptable only when
|
|
194
|
+
// the gate re-ran exactly those timeout-only failures clean (#1673).
|
|
195
|
+
stubbedChecks.length === 0 && !(report.test.failed > 0 && !cleanRerun(report.test)) &&
|
|
196
|
+
report.test.suite_died !== true &&
|
|
85
197
|
['pass', 'skip'].includes(report.typecheck?.status) && ['pass', 'skip'].includes(report.lint?.status) &&
|
|
86
198
|
![report.test, report.typecheck, report.lint].some((check) => check.timed_out === true);
|
|
87
|
-
try {
|
|
199
|
+
if (emit) try {
|
|
88
200
|
const { emitEvent, sessionAttribution } = await import('./events.mjs');
|
|
89
201
|
const counts = admitSuiteCounts(report?.test);
|
|
90
202
|
const waveNumber = resolveWaveNumber(ledgerRoot);
|
|
91
203
|
await emitEvent(`orchestrator.quality_gate.${passed ? 'passed' : 'failed'}`, {
|
|
92
|
-
variant: 'full-gate', exit_code: passed ? 0 : 1, remote_host: alias,
|
|
204
|
+
variant: 'full-gate', exit_code: passed ? 0 : 1, remote_host: alias, host: alias, host_source: hostSource,
|
|
93
205
|
pushed_sha: sha, ...sessionAttribution(ledgerRoot),
|
|
94
206
|
...joinGateKillFields({ outer: {}, gateStdout: JSON.stringify(report) }),
|
|
95
207
|
...(counts ? { counts } : {}),
|
|
96
208
|
...(waveNumber !== null ? { wave_number: waveNumber } : {}),
|
|
97
209
|
...(report?.test?.failed_files?.length ? { failed_files: report.test.failed_files } : {}),
|
|
210
|
+
...(Array.isArray(report?.test?.rerun?.files) && report.test.rerun.files.length
|
|
211
|
+
? { rerun: { files: report.test.rerun.files, status: report.test.rerun.status } } : {}),
|
|
98
212
|
}, { repoRoot: ledgerRoot });
|
|
99
213
|
} catch { /* telemetry cannot change the blocking verdict */ }
|
|
100
|
-
|
|
101
|
-
return passed ? 0 : 1;
|
|
214
|
+
logFn(`remote quality gate ${passed ? 'passed' : 'FAILED — Push blocked'} (${alias}).`);
|
|
215
|
+
return { code: passed ? 0 : 1, passed, alias, report: report ?? null, hostSource };
|
|
102
216
|
}
|
|
103
|
-
|
|
104
|
-
|
|
217
|
+
if (hostSource === 'override') {
|
|
218
|
+
// An explicit override names where the gate may run; silently falling back to the
|
|
219
|
+
// local host is the case the override exists to prevent (#1626).
|
|
220
|
+
logFn(`no SO_GATE_HOSTS route could run the gate (${hosts.map((h) => h.alias).join(', ')}) — Push blocked (no local fallback).`);
|
|
221
|
+
return done(2);
|
|
222
|
+
}
|
|
223
|
+
logFn('remote routes unavailable — local fallback (host: local, host_source: fallback; an origin MR branch takes the partial gate, #1642).');
|
|
224
|
+
return done(10);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function main() {
|
|
228
|
+
const [tree, sha, ledgerRoot] = process.argv.slice(2);
|
|
229
|
+
const r = await runRemoteGate({ tree, sha, ledgerRoot });
|
|
230
|
+
return r.code;
|
|
105
231
|
}
|
|
106
232
|
|
|
107
233
|
if (isMainModule(import.meta.url)) process.exitCode = await main();
|