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
|
@@ -120,32 +120,11 @@
|
|
|
120
120
|
* executing the binary shows the constraint — same lesson as the `glab repo`
|
|
121
121
|
* asymmetry above, in the opposite direction.
|
|
122
122
|
*
|
|
123
|
-
* ##
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* `--repo`-present branch (see its own docstring for why), so the
|
|
129
|
-
* conditional-positional rule can only ever describe calls that LACK the flag —
|
|
130
|
-
* the harmless half of that table. The harmful half is invisible:
|
|
131
|
-
* `gh pr checks -R o/r --watch` carries `-R` with no positional, which gh 2.86.0
|
|
132
|
-
* rejects at parse time (`argument required when using the --repo flag`, exit 1,
|
|
133
|
-
* probed 2026-08-14). This census counts it as `withFlag` — as already hardened.
|
|
134
|
-
*
|
|
135
|
-
* Measured 2026-08-14 against this tree: **0 such call sites**. A probe copy of
|
|
136
|
-
* this file, patched to skip the repo flag's own value before scanning the
|
|
137
|
-
* remaining tokens for a positional, ran `inspectVcsRepoFlag(repoRoot)` and
|
|
138
|
-
* reported an empty list — so the blind spot is inert here today, and detecting
|
|
139
|
-
* it is ~8 lines.
|
|
140
|
-
*
|
|
141
|
-
* It is nonetheless NOT reported, for a contract reason rather than a detection
|
|
142
|
-
* one. A finding here means "add `--repo`"; that call's fix is the opposite
|
|
143
|
-
* ("name the PR, or drop the flag"), and the `kind` enum, the WARN message text
|
|
144
|
-
* and the PASS line's "N bare" all speak only the first language. Folding a
|
|
145
|
-
* loud, exit-1 failure into a census built for SILENT wrong-repo failures would
|
|
146
|
-
* hand the sweep a worklist whose entries prescribe opposite edits, and would
|
|
147
|
-
* buy nothing the command's own first run does not already say. That class wants
|
|
148
|
-
* its own check; it is routed as a follow-up rather than patched in here.
|
|
123
|
+
* ## Rejected flags (issue #1024)
|
|
124
|
+
*
|
|
125
|
+
* `check-vcs-repo-flag-positional.mjs` uses the same scan engine in rejected
|
|
126
|
+
* mode, with its own finding kind and advice: name the PR or drop the flag.
|
|
127
|
+
* The default census still reports only missing repo flags.
|
|
149
128
|
*
|
|
150
129
|
* ## Named coverage gaps (v1) — read these before quoting the count
|
|
151
130
|
*
|
|
@@ -195,6 +174,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
195
174
|
import path from 'node:path';
|
|
196
175
|
import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
|
|
197
176
|
import { isMainModule } from '../is-main-module.mjs';
|
|
177
|
+
import { tokenizeCommand, redirectSpanEnd } from '../command-blocker.mjs';
|
|
198
178
|
|
|
199
179
|
/** Directories whose content is scanned. Root-level `*.md` is added separately. */
|
|
200
180
|
const SCAN_DIRS = Object.freeze([
|
|
@@ -309,12 +289,8 @@ function looksPositional(token) {
|
|
|
309
289
|
* silence, because a wrongly-suppressed call site is invisible forever while a
|
|
310
290
|
* wrongly-reported one is one sweep read away:
|
|
311
291
|
*
|
|
312
|
-
* 1.
|
|
313
|
-
*
|
|
314
|
-
* mean parsing which flags consume a value — `gh pr merge -R o/r 123` puts
|
|
315
|
-
* its positional at index 4, behind `-R`'s argument. What that ordering
|
|
316
|
-
* costs is named in § Blind spot at the top of this file: the `-R`-present,
|
|
317
|
-
* positional-absent call — the one the CLI actually rejects — is invisible.
|
|
292
|
+
* 1. The bare census consults this AFTER the repo-flag branch. The separate
|
|
293
|
+
* rejected-flag check first removes option values from the token stream.
|
|
318
294
|
* 2. ANY positional-looking token past the subcommand disarms the rule. A flag
|
|
319
295
|
* VALUE (`--json state` → `state`) can be mistaken for a positional; that
|
|
320
296
|
* mistake yields a finding, never a suppression.
|
|
@@ -331,6 +307,55 @@ function repoFlagBlockedByMissingPositional(cli, group, sub, rest) {
|
|
|
331
307
|
return !rest.some(looksPositional);
|
|
332
308
|
}
|
|
333
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Known value-taking options of the six conditional gh pr subcommands.
|
|
312
|
+
* Unknown options retain the census's conservative positional heuristic;
|
|
313
|
+
* revisit this list when gh adds a value-taking option to these commands.
|
|
314
|
+
*/
|
|
315
|
+
const PR_VALUE_FLAGS = new Set([
|
|
316
|
+
'-R', '--repo', '--json', '-q', '--jq', '-t', '--template', '-i', '--interval',
|
|
317
|
+
'--color', '-A', '--author-email', '-b', '--body', '-F', '--body-file',
|
|
318
|
+
'--subject', '--match-head-commit',
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
/** @param {string[]} args @returns {boolean} a repo option, not another option's value */
|
|
322
|
+
function repoFlagPresent(args) {
|
|
323
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
324
|
+
const arg = args[index];
|
|
325
|
+
if (arg === '--') break;
|
|
326
|
+
if (arg === '-R' || arg === '--repo' || arg.startsWith('--repo=') || /^-R.+/.test(arg)) return true;
|
|
327
|
+
if (PR_VALUE_FLAGS.has(arg)) index += 1;
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** @param {string[]} args @returns {string[]} args without option values */
|
|
333
|
+
function withoutOptionValues(args) {
|
|
334
|
+
const rest = [];
|
|
335
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
336
|
+
const arg = args[index];
|
|
337
|
+
if (arg === '--') return rest.concat(args.slice(index + 1));
|
|
338
|
+
if (PR_VALUE_FLAGS.has(arg)) index += 1;
|
|
339
|
+
else if (!arg.startsWith('-')) rest.push(arg);
|
|
340
|
+
}
|
|
341
|
+
return rest;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Strip shell comments at word boundaries, retaining quoted/escaped hashes.
|
|
345
|
+
* @param {string} text @returns {string}
|
|
346
|
+
*/
|
|
347
|
+
function stripShellComment(text) {
|
|
348
|
+
let quote = '';
|
|
349
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
350
|
+
const char = text[index];
|
|
351
|
+
if (char === '\\' && quote !== "'") { index += 1; continue; }
|
|
352
|
+
if (quote) { if (char === quote) quote = ''; continue; }
|
|
353
|
+
if (char === '"' || char === "'" || char === '`') { quote = char; continue; }
|
|
354
|
+
if (char === '#' && (index === 0 || /[\s;|&()]/.test(text[index - 1]))) return text.slice(0, index);
|
|
355
|
+
}
|
|
356
|
+
return text;
|
|
357
|
+
}
|
|
358
|
+
|
|
334
359
|
/**
|
|
335
360
|
* A `--repo`/`-R` occurrence. `--repo=x`, `-R x` and `-R"$SPEC"` all count;
|
|
336
361
|
* `--repository` does not (the `\b` blocks it) and neither does a `-R` glued to
|
|
@@ -366,44 +391,60 @@ const COMMAND_RE = /(?:^|[|;&(]|\$\()\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(gh|gl
|
|
|
366
391
|
* outer command's own, substitution-valued or not: `-R $(cat repo.txt)`
|
|
367
392
|
* counts.
|
|
368
393
|
*
|
|
369
|
-
* The outer projection replaces each substitution with
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
* Two named ceilings, both landing on today's verdict so neither is a
|
|
374
|
-
* regression; revisit if a call site in this corpus ever takes either shape.
|
|
375
|
-
* `$((` arithmetic expansion reads as a substitution plus a plain paren, so a
|
|
376
|
-
* segment containing one is cut at its second `)` — exactly where the previous
|
|
377
|
-
* `)`-stop cut it. Quote state is not tracked: `'$(x)'` is a literal string to a
|
|
378
|
-
* real shell and a substitution to this walk, which reaches the same verdict
|
|
379
|
-
* (the `-R` inside is not this command's flag) by a more generous route.
|
|
394
|
+
* The outer projection replaces each substitution with an opaque argument
|
|
395
|
+
* placeholder. Only placeholders in possible PR positions suppress a finding;
|
|
396
|
+
* option values and redirect operands are removed first. Quote and
|
|
397
|
+
* escape state are evaluated before command separators can end the segment.
|
|
380
398
|
*
|
|
381
399
|
* @param {string} rest text following the matched CLI token
|
|
382
|
-
* @returns {{segment: string, outer: string}} full segment, and its depth-0 projection
|
|
400
|
+
* @returns {{segment: string, outer: string, unresolved: boolean}} full segment, and its depth-0 projection
|
|
383
401
|
*/
|
|
384
402
|
function sliceInvocation(rest) {
|
|
385
403
|
let segment = '';
|
|
386
404
|
let outer = '';
|
|
387
405
|
let depth = 0;
|
|
388
406
|
let inBacktick = false;
|
|
407
|
+
let quote = '';
|
|
408
|
+
let unresolved = false;
|
|
389
409
|
|
|
390
410
|
for (let index = 0; index < rest.length; index += 1) {
|
|
391
411
|
const char = rest[index];
|
|
392
412
|
const pair = rest.slice(index, index + 2);
|
|
393
413
|
|
|
394
414
|
if (depth === 0 && !inBacktick) {
|
|
395
|
-
if (
|
|
396
|
-
|
|
415
|
+
if (char === '\\' && quote !== "'") {
|
|
416
|
+
segment += rest.slice(index, index + 2);
|
|
417
|
+
outer += rest.slice(index, index + 2);
|
|
418
|
+
index += 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (char === quote) {
|
|
422
|
+
quote = '';
|
|
423
|
+
segment += char;
|
|
424
|
+
outer += char;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (!quote && (char === '"' || char === "'")) {
|
|
428
|
+
quote = char;
|
|
429
|
+
segment += char;
|
|
430
|
+
outer += char;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (!quote && (pair === '||' || pair === '&&' ||
|
|
434
|
+
(char === '|' && rest[index - 1] !== '>') || char === ';' || char === ')')) break;
|
|
435
|
+
if (quote !== "'" && pair === '$(') {
|
|
436
|
+
unresolved = true;
|
|
397
437
|
depth = 1;
|
|
398
438
|
segment += pair;
|
|
399
|
-
outer += '
|
|
439
|
+
outer += '__SO_UNRESOLVED__';
|
|
400
440
|
index += 1;
|
|
401
441
|
continue;
|
|
402
442
|
}
|
|
403
|
-
if (char === '`') {
|
|
443
|
+
if (quote !== "'" && char === '`') {
|
|
444
|
+
unresolved = true;
|
|
404
445
|
inBacktick = true;
|
|
405
446
|
segment += char;
|
|
406
|
-
outer += '
|
|
447
|
+
outer += '__SO_UNRESOLVED__';
|
|
407
448
|
continue;
|
|
408
449
|
}
|
|
409
450
|
segment += char;
|
|
@@ -425,7 +466,7 @@ function sliceInvocation(rest) {
|
|
|
425
466
|
if (char === ')') depth -= 1;
|
|
426
467
|
}
|
|
427
468
|
|
|
428
|
-
return { segment, outer };
|
|
469
|
+
return { segment, outer, unresolved };
|
|
429
470
|
}
|
|
430
471
|
|
|
431
472
|
/** Snippet budget — see § stdout budget at the CLI guard. */
|
|
@@ -433,7 +474,7 @@ const SNIPPET_MAX = 200;
|
|
|
433
474
|
|
|
434
475
|
/**
|
|
435
476
|
* @typedef {{
|
|
436
|
-
* kind: 'missing-repo-flag-doc' | 'missing-repo-flag-code' | 'tool-error',
|
|
477
|
+
* kind: 'missing-repo-flag-doc' | 'missing-repo-flag-code' | 'repo-flag-missing-positional' | 'tool-error',
|
|
437
478
|
* file: string,
|
|
438
479
|
* line: number,
|
|
439
480
|
* cli: 'gh' | 'glab' | '-',
|
|
@@ -490,9 +531,11 @@ function clampSnippet(text) {
|
|
|
490
531
|
*
|
|
491
532
|
* @param {string} text the logical command line
|
|
492
533
|
* @param {Tally} tally mutated in place
|
|
534
|
+
* @param {boolean} [rejected] collect rejected flags instead of missing flags
|
|
493
535
|
* @returns {{cli: 'gh'|'glab', command: string, segment: string}[]} findings-worthy invocations
|
|
494
536
|
*/
|
|
495
|
-
export function extractBareInvocations(text, tally) {
|
|
537
|
+
export function extractBareInvocations(text, tally, rejected = false) {
|
|
538
|
+
text = stripShellComment(text);
|
|
496
539
|
/** @type {{cli: 'gh'|'glab', command: string, segment: string}[]} */
|
|
497
540
|
const bare = [];
|
|
498
541
|
COMMAND_RE.lastIndex = 0;
|
|
@@ -521,10 +564,20 @@ export function extractBareInvocations(text, tally) {
|
|
|
521
564
|
tally.applicable += 1;
|
|
522
565
|
// `outer`, never `segment`: a `-R` inside a command substitution belongs to
|
|
523
566
|
// the inner command — see `sliceInvocation` job 2.
|
|
524
|
-
|
|
567
|
+
const argv = rejected ? tokenizeCommand(outer) : [];
|
|
568
|
+
const args = [];
|
|
569
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
570
|
+
if (argv[index].redirect) index = redirectSpanEnd(argv, index);
|
|
571
|
+
else args.push(argv[index].text);
|
|
572
|
+
}
|
|
573
|
+
if (rejected ? repoFlagPresent(args.slice(2)) : REPO_FLAG_RE.test(outer)) {
|
|
525
574
|
tally.withFlag += 1;
|
|
575
|
+
if (rejected && repoFlagBlockedByMissingPositional(cli, head, tokens[1], withoutOptionValues(args.slice(2)))) {
|
|
576
|
+
bare.push({ cli, command: `${cli} ${head} ${tokens[1]}`, segment });
|
|
577
|
+
}
|
|
526
578
|
continue;
|
|
527
579
|
}
|
|
580
|
+
if (rejected) continue;
|
|
528
581
|
if (repoFlagBlockedByMissingPositional(cli, head, tokens[1], tokens.slice(2))) {
|
|
529
582
|
tally.positionalRequired += 1;
|
|
530
583
|
continue;
|
|
@@ -560,6 +613,73 @@ function joinContinuations(entries) {
|
|
|
560
613
|
return joined;
|
|
561
614
|
}
|
|
562
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Read the heredoc delimiter word starting at `start` and apply shell quote
|
|
618
|
+
* removal: `\EOF`, `'EOF'`, `"EOF"` and `E"O"F` all name the terminator `EOF`.
|
|
619
|
+
* @param {string} text @param {number} start
|
|
620
|
+
* @returns {{word: string, end: number}} `end` = index after the word
|
|
621
|
+
*/
|
|
622
|
+
function readDelimiterWord(text, start) {
|
|
623
|
+
let word = '';
|
|
624
|
+
let index = start;
|
|
625
|
+
for (; index < text.length; index += 1) {
|
|
626
|
+
const char = text[index];
|
|
627
|
+
if (/[\s;|&<>()]/.test(char)) break;
|
|
628
|
+
if (char === '\\') { index += 1; word += text[index] ?? ''; continue; }
|
|
629
|
+
if (char === "'" || char === '"') {
|
|
630
|
+
const close = text.indexOf(char, index + 1);
|
|
631
|
+
const stop = close === -1 ? text.length : close;
|
|
632
|
+
const quoted = text.slice(index + 1, stop);
|
|
633
|
+
word += char === '"' ? quoted.replace(/\\([\\"$`])/g, '$1') : quoted;
|
|
634
|
+
index = stop;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
word += char;
|
|
638
|
+
}
|
|
639
|
+
return { word, end: index };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Index just past the `))` closing the arithmetic `((` at `start`, so a shift
|
|
644
|
+
* operator inside `$(( ))` / `(( ))` is never read as a heredoc.
|
|
645
|
+
* @param {string} text @param {number} start @returns {number}
|
|
646
|
+
*/
|
|
647
|
+
function arithmeticEnd(text, start) {
|
|
648
|
+
let depth = 0;
|
|
649
|
+
for (let index = start; index < text.length; index += 1) {
|
|
650
|
+
if (text[index] === '(') depth += 1;
|
|
651
|
+
else if (text[index] === ')' && --depth === 0) return index + 1;
|
|
652
|
+
}
|
|
653
|
+
return text.length;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Find heredoc delimiters outside quoted/escaped shell text and arithmetic.
|
|
658
|
+
* Their bodies are unresolved data for the positional check, including an
|
|
659
|
+
* unterminated body.
|
|
660
|
+
* @param {string} text
|
|
661
|
+
* @returns {{word: string, stripTabs: boolean}[]}
|
|
662
|
+
*/
|
|
663
|
+
function heredocDelimiters(text) {
|
|
664
|
+
const delimiters = [];
|
|
665
|
+
let quote = '';
|
|
666
|
+
text = stripShellComment(text);
|
|
667
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
668
|
+
const char = text[index];
|
|
669
|
+
if (char === '\\' && quote !== "'") { index += 1; continue; }
|
|
670
|
+
if (quote) { if (char === quote) quote = ''; continue; }
|
|
671
|
+
if (char === '"' || char === "'" || char === '`') { quote = char; continue; }
|
|
672
|
+
if (text.startsWith('((', index)) { index = arithmeticEnd(text, index) - 1; continue; }
|
|
673
|
+
if (text.startsWith('<<<', index)) { index += 2; continue; }
|
|
674
|
+
if (!text.startsWith('<<', index)) continue;
|
|
675
|
+
const head = /^<<(-?)[ \t]*/.exec(text.slice(index));
|
|
676
|
+
const { word, end } = readDelimiterWord(text, index + head[0].length);
|
|
677
|
+
if (word) delimiters.push({ word, stripTabs: head[1] === '-' });
|
|
678
|
+
index = Math.max(index + 1, end - 1);
|
|
679
|
+
}
|
|
680
|
+
return delimiters;
|
|
681
|
+
}
|
|
682
|
+
|
|
563
683
|
/**
|
|
564
684
|
* Scan a markdown file: fence state machine → shell fences only → drop shell
|
|
565
685
|
* comments → join continuations → classify.
|
|
@@ -567,30 +687,50 @@ function joinContinuations(entries) {
|
|
|
567
687
|
* @param {string} relative repo-relative path
|
|
568
688
|
* @param {string} body file content
|
|
569
689
|
* @param {Tally} tally mutated in place
|
|
690
|
+
* @param {boolean} [rejected] collect rejected flags instead of missing flags
|
|
570
691
|
* @returns {Finding[]}
|
|
571
692
|
*/
|
|
572
|
-
export function scanMarkdown(relative, body, tally) {
|
|
693
|
+
export function scanMarkdown(relative, body, tally, rejected = false) {
|
|
573
694
|
/** @type {Finding[]} */
|
|
574
695
|
const findings = [];
|
|
575
696
|
/** @type {{line: number, text: string}[]} */
|
|
576
697
|
const shellLines = [];
|
|
698
|
+
/** @type {{word: string, stripTabs: boolean}[]} */
|
|
699
|
+
let pendingHeredocs = [];
|
|
700
|
+
// Delimiters of the logical line still being continued: a body starts only
|
|
701
|
+
// after the line that ends the `\`-continued command (same test as joinContinuations).
|
|
702
|
+
/** @type {{word: string, stripTabs: boolean}[]} */
|
|
703
|
+
let lineHeredocs = [];
|
|
704
|
+
let previousLine = 0;
|
|
577
705
|
|
|
578
706
|
forEachLine(body, (raw, { lineNumber, inFence, lang }) => {
|
|
579
707
|
if (!inFence || !SHELL_LANGS.has(lang)) return;
|
|
708
|
+
// Fence markers are omitted by forEachLine; a gap resets shell context.
|
|
709
|
+
if (lineNumber !== previousLine + 1) { pendingHeredocs = []; lineHeredocs = []; }
|
|
710
|
+
previousLine = lineNumber;
|
|
711
|
+
if (rejected && pendingHeredocs.length) {
|
|
712
|
+
const pending = pendingHeredocs[0];
|
|
713
|
+
if ((pending.stripTabs ? raw.replace(/^\t+/, '') : raw) === pending.word) pendingHeredocs.shift();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
580
716
|
const stripped = raw.replace(/^\s*[$❯>]\s+/, '');
|
|
581
717
|
if (/^\s*#/.test(stripped)) return;
|
|
718
|
+
if (rejected) {
|
|
719
|
+
lineHeredocs.push(...heredocDelimiters(stripped));
|
|
720
|
+
if (!/\\\s*$/.test(stripped)) { pendingHeredocs = lineHeredocs; lineHeredocs = []; }
|
|
721
|
+
}
|
|
582
722
|
shellLines.push({ line: lineNumber, text: stripped });
|
|
583
723
|
});
|
|
584
724
|
|
|
585
725
|
for (const entry of joinContinuations(shellLines)) {
|
|
586
|
-
for (const hit of extractBareInvocations(entry.text, tally)) {
|
|
726
|
+
for (const hit of extractBareInvocations(entry.text, tally, rejected)) {
|
|
587
727
|
findings.push({
|
|
588
|
-
kind: 'missing-repo-flag-doc',
|
|
728
|
+
kind: rejected ? 'repo-flag-missing-positional' : 'missing-repo-flag-doc',
|
|
589
729
|
file: relative,
|
|
590
730
|
line: entry.line,
|
|
591
731
|
cli: hit.cli,
|
|
592
732
|
command: hit.command,
|
|
593
|
-
message:
|
|
733
|
+
message: rejected ? rejectedFlagMessage(hit.command) :
|
|
594
734
|
`\`${hit.command}\` runs without --repo/-R — the target project comes from the ambient ` +
|
|
595
735
|
`cwd remote: ${clampSnippet(`${hit.cli}${hit.segment}`)}`,
|
|
596
736
|
});
|
|
@@ -628,8 +768,32 @@ export function scanArgvCalls(body) {
|
|
|
628
768
|
}
|
|
629
769
|
let depth = 0;
|
|
630
770
|
let end = -1;
|
|
771
|
+
let quote = '';
|
|
772
|
+
let uncommented = '';
|
|
631
773
|
for (let index = 0; index < after.length; index += 1) {
|
|
632
774
|
const char = after[index];
|
|
775
|
+
if (quote) {
|
|
776
|
+
uncommented += char;
|
|
777
|
+
if (char === '\\') { uncommented += after[index + 1] ?? ''; index += 1; }
|
|
778
|
+
else if (char === quote) quote = '';
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (after.startsWith('//', index)) {
|
|
782
|
+
const newline = after.indexOf('\n', index + 2);
|
|
783
|
+
if (newline === -1) break;
|
|
784
|
+
uncommented += ' ';
|
|
785
|
+
index = newline;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (after.startsWith('/*', index)) {
|
|
789
|
+
const close = after.indexOf('*/', index + 2);
|
|
790
|
+
if (close === -1) break;
|
|
791
|
+
uncommented += ' ';
|
|
792
|
+
index = close + 1;
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
uncommented += char;
|
|
796
|
+
if (char === "'" || char === '"' || char === '`') { quote = char; continue; }
|
|
633
797
|
if (char === '[') depth += 1;
|
|
634
798
|
else if (char === ']') {
|
|
635
799
|
depth -= 1;
|
|
@@ -643,12 +807,25 @@ export function scanArgvCalls(body) {
|
|
|
643
807
|
unresolved += 1;
|
|
644
808
|
continue;
|
|
645
809
|
}
|
|
646
|
-
const inner =
|
|
810
|
+
const inner = uncommented.slice(1, -1);
|
|
647
811
|
if (inner.includes('...') || /\$\{/.test(inner)) {
|
|
648
812
|
unresolved += 1;
|
|
649
813
|
continue;
|
|
650
814
|
}
|
|
651
|
-
|
|
815
|
+
// Consume the entire array, never just its quoted fragments. Escaped or
|
|
816
|
+
// computed strings are deliberately unresolved rather than partly decoded.
|
|
817
|
+
const literal = /\s*(?:'([^'\\]*)'|"([^"\\]*)")\s*(?:,|$)/gy;
|
|
818
|
+
const args = [];
|
|
819
|
+
let consumed = 0;
|
|
820
|
+
let item;
|
|
821
|
+
while ((item = literal.exec(inner)) !== null) {
|
|
822
|
+
args.push(item[1] ?? item[2]);
|
|
823
|
+
consumed = literal.lastIndex;
|
|
824
|
+
}
|
|
825
|
+
if (inner.slice(consumed).trim()) {
|
|
826
|
+
unresolved += 1;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
652
829
|
literals.push({ index: matched.index, cli: /** @type {'gh'|'glab'} */ (matched[1]), args });
|
|
653
830
|
}
|
|
654
831
|
return { literals, unresolved };
|
|
@@ -662,9 +839,10 @@ export function scanArgvCalls(body) {
|
|
|
662
839
|
* @param {string} relative repo-relative path
|
|
663
840
|
* @param {string} body file content
|
|
664
841
|
* @param {Tally} tally mutated in place
|
|
842
|
+
* @param {boolean} [rejected] collect rejected flags instead of missing flags
|
|
665
843
|
* @returns {{findings: Finding[], unresolvedArgv: number}}
|
|
666
844
|
*/
|
|
667
|
-
export function scanCode(relative, body, tally) {
|
|
845
|
+
export function scanCode(relative, body, tally, rejected = false) {
|
|
668
846
|
/** @type {Finding[]} */
|
|
669
847
|
const findings = [];
|
|
670
848
|
const lines = body.split('\n');
|
|
@@ -681,14 +859,14 @@ export function scanCode(relative, body, tally) {
|
|
|
681
859
|
const closeAt = raw.indexOf(quote, contentStart);
|
|
682
860
|
const literal = closeAt === -1 ? raw.slice(contentStart) : raw.slice(contentStart, closeAt);
|
|
683
861
|
if (literal.includes('${')) continue;
|
|
684
|
-
for (const hit of extractBareInvocations(literal.trim(), tally)) {
|
|
862
|
+
for (const hit of extractBareInvocations(literal.trim(), tally, rejected)) {
|
|
685
863
|
findings.push({
|
|
686
|
-
kind: 'missing-repo-flag-code',
|
|
864
|
+
kind: rejected ? 'repo-flag-missing-positional' : 'missing-repo-flag-code',
|
|
687
865
|
file: relative,
|
|
688
866
|
line: index + 1,
|
|
689
867
|
cli: hit.cli,
|
|
690
868
|
command: hit.command,
|
|
691
|
-
message:
|
|
869
|
+
message: rejected ? rejectedFlagMessage(hit.command) :
|
|
692
870
|
`\`${hit.command}\` in a shell-command string runs without --repo/-R — the target project ` +
|
|
693
871
|
`comes from the ambient cwd remote: ${clampSnippet(`${hit.cli}${hit.segment}`)}`,
|
|
694
872
|
});
|
|
@@ -712,11 +890,13 @@ export function scanCode(relative, body, tally) {
|
|
|
712
890
|
continue;
|
|
713
891
|
}
|
|
714
892
|
tally.applicable += 1;
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
893
|
+
const hasRepoFlag = rejected ? repoFlagPresent(call.args.slice(2)) :
|
|
894
|
+
call.args.some((arg) => arg === '--repo' || arg === '-R' || arg.startsWith('--repo='));
|
|
895
|
+
if (hasRepoFlag) tally.withFlag += 1;
|
|
896
|
+
if (rejected) {
|
|
897
|
+
if (!hasRepoFlag || !repoFlagBlockedByMissingPositional(call.cli, head, call.args[1], withoutOptionValues(call.args.slice(2)))) continue;
|
|
898
|
+
} else if (hasRepoFlag) continue;
|
|
899
|
+
if (!rejected && repoFlagBlockedByMissingPositional(call.cli, head, call.args[1], call.args.slice(2))) {
|
|
720
900
|
tally.positionalRequired += 1;
|
|
721
901
|
continue;
|
|
722
902
|
}
|
|
@@ -727,12 +907,12 @@ export function scanCode(relative, body, tally) {
|
|
|
727
907
|
const line = body.slice(0, call.index).split('\n').length;
|
|
728
908
|
const second = call.args[1] && !call.args[1].startsWith('-') ? ` ${call.args[1]}` : '';
|
|
729
909
|
findings.push({
|
|
730
|
-
kind: 'missing-repo-flag-code',
|
|
910
|
+
kind: rejected ? 'repo-flag-missing-positional' : 'missing-repo-flag-code',
|
|
731
911
|
file: relative,
|
|
732
912
|
line,
|
|
733
913
|
cli: call.cli,
|
|
734
914
|
command: `${call.cli} ${head}${second}`,
|
|
735
|
-
message:
|
|
915
|
+
message: rejected ? rejectedFlagMessage(`${call.cli} ${head}${second}`) :
|
|
736
916
|
`\`${call.cli} ${head}${second}\` argv array carries no --repo/-R — the target project comes ` +
|
|
737
917
|
`from the ambient cwd remote: ${clampSnippet(call.args.join(' '))}`,
|
|
738
918
|
});
|
|
@@ -741,10 +921,16 @@ export function scanCode(relative, body, tally) {
|
|
|
741
921
|
return { findings, unresolvedArgv: unresolved };
|
|
742
922
|
}
|
|
743
923
|
|
|
924
|
+
/** @param {string} command @returns {string} */
|
|
925
|
+
function rejectedFlagMessage(command) {
|
|
926
|
+
return `\`${command}\`: --repo/-R breaks this call — name the PR positional or drop the flag`;
|
|
927
|
+
}
|
|
928
|
+
|
|
744
929
|
/**
|
|
745
930
|
* Run the full census.
|
|
746
931
|
*
|
|
747
932
|
* @param {string} pluginRoot absolute plugin root
|
|
933
|
+
* @param {{rejected?: boolean}} [options] separate rejected-flag classification
|
|
748
934
|
* @returns {{
|
|
749
935
|
* ok: boolean,
|
|
750
936
|
* summary: {
|
|
@@ -758,7 +944,7 @@ export function scanCode(relative, body, tally) {
|
|
|
758
944
|
* toolError: boolean,
|
|
759
945
|
* }}
|
|
760
946
|
*/
|
|
761
|
-
export function inspectVcsRepoFlag(pluginRoot) {
|
|
947
|
+
export function inspectVcsRepoFlag(pluginRoot, { rejected = false } = {}) {
|
|
762
948
|
/** @type {Finding[]} */
|
|
763
949
|
const findings = [];
|
|
764
950
|
/** @type {Tally} */
|
|
@@ -839,10 +1025,10 @@ export function inspectVcsRepoFlag(pluginRoot) {
|
|
|
839
1025
|
}
|
|
840
1026
|
if (DOC_EXTENSIONS.includes(path.extname(absolute))) {
|
|
841
1027
|
docFiles += 1;
|
|
842
|
-
findings.push(...scanMarkdown(relative, body, tally));
|
|
1028
|
+
findings.push(...scanMarkdown(relative, body, tally, rejected));
|
|
843
1029
|
} else {
|
|
844
1030
|
codeFiles += 1;
|
|
845
|
-
const scanned = scanCode(relative, body, tally);
|
|
1031
|
+
const scanned = scanCode(relative, body, tally, rejected);
|
|
846
1032
|
findings.push(...scanned.findings);
|
|
847
1033
|
unresolvedArgv += scanned.unresolvedArgv;
|
|
848
1034
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Usage: check-workflow-continue-on-error.mjs <plugin-root>
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
7
|
+
|
|
8
|
+
function inspectWorkflow(yamlText, relPath) {
|
|
9
|
+
const workflow = yaml.load(yamlText, { schema: yaml.CORE_SCHEMA });
|
|
10
|
+
const jobs = Object.entries(workflow?.jobs ?? {});
|
|
11
|
+
const findings = jobs
|
|
12
|
+
.filter(([, job]) => job !== null && job !== undefined && Object.hasOwn(job, 'continue-on-error')
|
|
13
|
+
&& job['continue-on-error'] !== false)
|
|
14
|
+
.map(([job, config]) => ({ file: relPath, job, value: config['continue-on-error'] }));
|
|
15
|
+
return { findings, jobCount: jobs.length };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Find job-level tolerance that can leave a failed check run; invalid YAML throws. */
|
|
19
|
+
export function findJobLevelContinueOnError(yamlText, relPath) {
|
|
20
|
+
return inspectWorkflow(yamlText, relPath).findings;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function run(pluginRoot) {
|
|
24
|
+
console.log('--- Check: workflow job-level continue-on-error ---');
|
|
25
|
+
const workflowsDir = join(pluginRoot, '.github', 'workflows');
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
if (!existsSync(workflowsDir)) {
|
|
29
|
+
console.log(' PASS: no workflows');
|
|
30
|
+
passed++;
|
|
31
|
+
} else {
|
|
32
|
+
for (const entry of readdirSync(workflowsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
33
|
+
if (!entry.isFile() || !/\.ya?ml$/.test(entry.name)) continue;
|
|
34
|
+
const relPath = `.github/workflows/${entry.name}`;
|
|
35
|
+
try {
|
|
36
|
+
const { findings, jobCount } = inspectWorkflow(readFileSync(join(workflowsDir, entry.name), 'utf8'), relPath);
|
|
37
|
+
for (const { job } of findings) {
|
|
38
|
+
console.log(` FAIL: ${relPath}: job '${job}' sets job-level continue-on-error — its check run still concludes failure; move it to the step(s)`);
|
|
39
|
+
failed++;
|
|
40
|
+
}
|
|
41
|
+
if (findings.length === 0) {
|
|
42
|
+
console.log(` PASS: ${relPath}: no job-level continue-on-error (${jobCount} jobs)`);
|
|
43
|
+
passed++;
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.log(` FAIL: ${relPath}: ${error.message}`);
|
|
47
|
+
failed++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
|
52
|
+
return failed > 0 ? 1 : 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isMain = isMainModule(import.meta.url);
|
|
56
|
+
if (isMain) {
|
|
57
|
+
const root = process.argv[2];
|
|
58
|
+
if (!root) {
|
|
59
|
+
console.error('Usage: check-workflow-continue-on-error.mjs <plugin-root>');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
process.exit(run(root));
|
|
63
|
+
}
|
|
@@ -103,7 +103,9 @@ const DEFAULT_DEPS = {
|
|
|
103
103
|
function parseNames(raw, label, d) {
|
|
104
104
|
let parsed;
|
|
105
105
|
try {
|
|
106
|
-
|
|
106
|
+
// #1613: Windows PowerShell 5.1 (`Set-Content -Encoding utf8`) writes a UTF-8 BOM,
|
|
107
|
+
// which JSON.parse rejects — and a rejected names file silently switches CP11 off.
|
|
108
|
+
parsed = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
|
|
107
109
|
} catch (err) {
|
|
108
110
|
// Fix 3 (security-reviewer): NEVER embed err.message — V8's JSON.parse error
|
|
109
111
|
// text echoes the first ~10 chars of the file body, which for a confidential-
|