@event4u/agent-config 11.0.0 → 12.1.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 +1 -1
- package/CHANGELOG.md +173 -229
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/commands/agent-handoff.md +0 -1
- package/dist/agent-src/commands/agent-status.md +0 -1
- package/dist/agent-src/commands/agents/audit.md +0 -1
- package/dist/agent-src/commands/agents/init.md +0 -1
- package/dist/agent-src/commands/agents/optimize.md +0 -1
- package/dist/agent-src/commands/agents/user/accept.md +0 -1
- package/dist/agent-src/commands/agents/user/delete.md +0 -1
- package/dist/agent-src/commands/agents/user/init.md +0 -1
- package/dist/agent-src/commands/agents/user/review.md +0 -1
- package/dist/agent-src/commands/agents/user/show.md +0 -1
- package/dist/agent-src/commands/agents/user/update.md +0 -1
- package/dist/agent-src/commands/agents/user.md +0 -1
- package/dist/agent-src/commands/agents.md +0 -1
- package/dist/agent-src/commands/analytics/prune.md +0 -1
- package/dist/agent-src/commands/analytics/show.md +0 -1
- package/dist/agent-src/commands/analytics.md +0 -1
- package/dist/agent-src/commands/analyze/conformance.md +0 -1
- package/dist/agent-src/commands/analyze/decision.md +0 -1
- package/dist/agent-src/commands/analyze/inbox.md +0 -1
- package/dist/agent-src/commands/analyze/incident.md +0 -1
- package/dist/agent-src/commands/analyze/near-miss.md +0 -1
- package/dist/agent-src/commands/analyze/postmortem.md +0 -1
- package/dist/agent-src/commands/analyze/premortem.md +0 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +0 -1
- package/dist/agent-src/commands/analyze.md +0 -1
- package/dist/agent-src/commands/brand/identity.md +0 -1
- package/dist/agent-src/commands/brand/review.md +0 -1
- package/dist/agent-src/commands/brand/strategy.md +0 -1
- package/dist/agent-src/commands/brand/tokens.md +0 -1
- package/dist/agent-src/commands/brand/voice.md +0 -1
- package/dist/agent-src/commands/brand.md +0 -1
- package/dist/agent-src/commands/bug/fix.md +0 -1
- package/dist/agent-src/commands/bug/investigate.md +0 -1
- package/dist/agent-src/commands/bug.md +0 -1
- package/dist/agent-src/commands/challenge-me/vision.md +0 -1
- package/dist/agent-src/commands/challenge-me/with-docs.md +0 -1
- package/dist/agent-src/commands/challenge-me.md +0 -1
- package/dist/agent-src/commands/chat-history/import.md +0 -1
- package/dist/agent-src/commands/chat-history.md +0 -1
- package/dist/agent-src/commands/check-current-md.md +0 -1
- package/dist/agent-src/commands/commit/in-chunks.md +0 -1
- package/dist/agent-src/commands/commit.md +0 -1
- package/dist/agent-src/commands/condense.md +0 -1
- package/dist/agent-src/commands/context/create.md +0 -1
- package/dist/agent-src/commands/context/refactor.md +0 -1
- package/dist/agent-src/commands/context.md +0 -1
- package/dist/agent-src/commands/contribution-precheck.md +0 -1
- package/dist/agent-src/commands/cost/profile.md +0 -1
- package/dist/agent-src/commands/cost/report.md +0 -1
- package/dist/agent-src/commands/cost.md +0 -1
- package/dist/agent-src/commands/council/analysis.md +0 -1
- package/dist/agent-src/commands/council/debate.md +0 -1
- package/dist/agent-src/commands/council/default.md +0 -1
- package/dist/agent-src/commands/council/design.md +0 -1
- package/dist/agent-src/commands/council/optimize.md +0 -1
- package/dist/agent-src/commands/council/pr.md +0 -1
- package/dist/agent-src/commands/council.md +0 -1
- package/dist/agent-src/commands/design-system/capture.md +0 -1
- package/dist/agent-src/commands/design-system/generate.md +0 -1
- package/dist/agent-src/commands/design-system/import.md +0 -1
- package/dist/agent-src/commands/design-system.md +0 -1
- package/dist/agent-src/commands/estimate-ticket.md +0 -1
- package/dist/agent-src/commands/explain-run.md +0 -1
- package/dist/agent-src/commands/feature/dev.md +0 -1
- package/dist/agent-src/commands/feature/explore.md +0 -1
- package/dist/agent-src/commands/feature/plan.md +0 -1
- package/dist/agent-src/commands/feature/refactor.md +0 -1
- package/dist/agent-src/commands/feature/roadmap.md +0 -1
- package/dist/agent-src/commands/feature.md +0 -1
- package/dist/agent-src/commands/fix/ci.md +0 -1
- package/dist/agent-src/commands/fix/comments.md +0 -1
- package/dist/agent-src/commands/fix/portability.md +0 -1
- package/dist/agent-src/commands/fix/pr-comments-loop.md +0 -1
- package/dist/agent-src/commands/fix/pr-comments.md +0 -1
- package/dist/agent-src/commands/fix/quality.md +0 -1
- package/dist/agent-src/commands/fix/refs.md +0 -1
- package/dist/agent-src/commands/fix/route.md +0 -1
- package/dist/agent-src/commands/fix/seeder.md +0 -1
- package/dist/agent-src/commands/fix.md +0 -1
- package/dist/agent-src/commands/ghostwriter/delete.md +0 -1
- package/dist/agent-src/commands/ghostwriter/fetch.md +0 -1
- package/dist/agent-src/commands/ghostwriter/list.md +0 -1
- package/dist/agent-src/commands/ghostwriter/show.md +0 -1
- package/dist/agent-src/commands/ghostwriter/write.md +0 -1
- package/dist/agent-src/commands/ghostwriter.md +0 -1
- package/dist/agent-src/commands/grill-me.md +0 -1
- package/dist/agent-src/commands/humanize.md +0 -1
- package/dist/agent-src/commands/image/analyse.md +0 -1
- package/dist/agent-src/commands/image/create.md +0 -1
- package/dist/agent-src/commands/image/verify.md +0 -1
- package/dist/agent-src/commands/image.md +0 -1
- package/dist/agent-src/commands/implement-ticket.md +0 -1
- package/dist/agent-src/commands/jira-ticket.md +0 -1
- package/dist/agent-src/commands/judge/on-diff.md +0 -1
- package/dist/agent-src/commands/judge/solo.md +0 -1
- package/dist/agent-src/commands/judge/steps.md +0 -1
- package/dist/agent-src/commands/judge.md +0 -1
- package/dist/agent-src/commands/knowledge/cross-repo.md +0 -1
- package/dist/agent-src/commands/knowledge/forget.md +0 -1
- package/dist/agent-src/commands/knowledge/ingest.md +0 -1
- package/dist/agent-src/commands/knowledge/list.md +0 -1
- package/dist/agent-src/commands/knowledge.md +0 -1
- package/dist/agent-src/commands/memory/add.md +0 -1
- package/dist/agent-src/commands/memory/learn-low-impact.md +0 -1
- package/dist/agent-src/commands/memory/load.md +0 -1
- package/dist/agent-src/commands/memory/mine-session.md +0 -1
- package/dist/agent-src/commands/memory/promote.md +0 -1
- package/dist/agent-src/commands/memory/propose.md +0 -1
- package/dist/agent-src/commands/memory.md +0 -1
- package/dist/agent-src/commands/mission/upgrade.md +0 -1
- package/dist/agent-src/commands/mode.md +0 -1
- package/dist/agent-src/commands/module/create.md +0 -1
- package/dist/agent-src/commands/module/explore.md +0 -1
- package/dist/agent-src/commands/module.md +0 -1
- package/dist/agent-src/commands/optimize/agents-dir.md +0 -1
- package/dist/agent-src/commands/optimize/augmentignore.md +0 -1
- package/dist/agent-src/commands/optimize/deep.md +0 -1
- package/dist/agent-src/commands/optimize/project.md +0 -1
- package/dist/agent-src/commands/optimize/prompt.md +0 -1
- package/dist/agent-src/commands/optimize/rtk.md +0 -1
- package/dist/agent-src/commands/optimize/skills.md +0 -1
- package/dist/agent-src/commands/optimize.md +0 -1
- package/dist/agent-src/commands/orchestrate.md +0 -1
- package/dist/agent-src/commands/override/create.md +0 -1
- package/dist/agent-src/commands/override/manage.md +0 -1
- package/dist/agent-src/commands/override.md +0 -1
- package/dist/agent-src/commands/package/reset.md +0 -1
- package/dist/agent-src/commands/package/test.md +0 -1
- package/dist/agent-src/commands/package.md +0 -1
- package/dist/agent-src/commands/post-as/ghostwriter.md +0 -1
- package/dist/agent-src/commands/post-as/me.md +0 -1
- package/dist/agent-src/commands/post-as.md +0 -1
- package/dist/agent-src/commands/pr/create/description-only.md +0 -1
- package/dist/agent-src/commands/pr/create.md +23 -9
- package/dist/agent-src/commands/prediction-pool.md +0 -1
- package/dist/agent-src/commands/prepare-for-review.md +0 -1
- package/dist/agent-src/commands/profile/activate.md +0 -1
- package/dist/agent-src/commands/profile/deactivate.md +0 -1
- package/dist/agent-src/commands/profile/show.md +0 -1
- package/dist/agent-src/commands/profile.md +0 -1
- package/dist/agent-src/commands/project/analyze.md +0 -1
- package/dist/agent-src/commands/project/health.md +0 -1
- package/dist/agent-src/commands/project.md +0 -1
- package/dist/agent-src/commands/refine-ticket.md +0 -1
- package/dist/agent-src/commands/research/deep.md +0 -1
- package/dist/agent-src/commands/research/report.md +0 -1
- package/dist/agent-src/commands/research.md +0 -1
- package/dist/agent-src/commands/review/changes.md +0 -1
- package/dist/agent-src/commands/review/routing.md +0 -1
- package/dist/agent-src/commands/review.md +0 -1
- package/dist/agent-src/commands/roadmap/ai-council.md +0 -1
- package/dist/agent-src/commands/roadmap/create.md +0 -1
- package/dist/agent-src/commands/roadmap/materialize.md +0 -1
- package/dist/agent-src/commands/roadmap/next.md +0 -1
- package/dist/agent-src/commands/roadmap/process-full.md +0 -1
- package/dist/agent-src/commands/roadmap/process-phase.md +0 -1
- package/dist/agent-src/commands/roadmap/process-step.md +0 -1
- package/dist/agent-src/commands/roadmap.md +0 -1
- package/dist/agent-src/commands/rule-compliance-audit.md +0 -1
- package/dist/agent-src/commands/security-audit-config.md +0 -1
- package/dist/agent-src/commands/skill/preview.md +0 -1
- package/dist/agent-src/commands/skill.md +0 -1
- package/dist/agent-src/commands/skills/discover.md +0 -1
- package/dist/agent-src/commands/skills.md +0 -1
- package/dist/agent-src/commands/sync/agent-settings.md +0 -1
- package/dist/agent-src/commands/sync/gitignore/fix.md +0 -1
- package/dist/agent-src/commands/sync/gitignore.md +0 -1
- package/dist/agent-src/commands/sync.md +0 -1
- package/dist/agent-src/commands/tdd/green.md +0 -1
- package/dist/agent-src/commands/tdd/red.md +0 -1
- package/dist/agent-src/commands/tdd/refactor.md +0 -1
- package/dist/agent-src/commands/tdd.md +0 -1
- package/dist/agent-src/commands/team/adversarial.md +0 -1
- package/dist/agent-src/commands/team/delegate.md +0 -1
- package/dist/agent-src/commands/team/review.md +0 -1
- package/dist/agent-src/commands/team/status.md +0 -1
- package/dist/agent-src/commands/team-knowledge/bootstrap.md +0 -1
- package/dist/agent-src/commands/team-knowledge/consolidate.md +0 -1
- package/dist/agent-src/commands/team-knowledge.md +0 -1
- package/dist/agent-src/commands/team.md +0 -1
- package/dist/agent-src/commands/tests/create.md +0 -1
- package/dist/agent-src/commands/tests/e2e-heal.md +0 -1
- package/dist/agent-src/commands/tests/e2e-plan.md +0 -1
- package/dist/agent-src/commands/tests/execute.md +0 -1
- package/dist/agent-src/commands/tests.md +0 -1
- package/dist/agent-src/commands/threat-model.md +0 -1
- package/dist/agent-src/commands/update-form-request-messages.md +0 -1
- package/dist/agent-src/commands/upstream-contribute.md +0 -1
- package/dist/agent-src/commands/video/from-script.md +0 -1
- package/dist/agent-src/commands/video/from-song.md +0 -1
- package/dist/agent-src/commands/video/scene.md +0 -1
- package/dist/agent-src/commands/video/stitch.md +0 -1
- package/dist/agent-src/commands/video/storyboard.md +0 -1
- package/dist/agent-src/commands/video.md +0 -1
- package/dist/agent-src/commands/work.md +0 -1
- package/dist/agent-src/commands/worktree/cleanup.md +0 -1
- package/dist/agent-src/commands/worktree/create.md +0 -1
- package/dist/agent-src/commands/worktree/status.md +0 -1
- package/dist/agent-src/commands/worktree/verify.md +0 -1
- package/dist/agent-src/commands/worktree.md +0 -1
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +32 -1
- package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +9 -1
- package/dist/agent-src/skills/pdf-tools/SKILL.md +1 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -7
- package/dist/agent-src/skills/skill-writing/SKILL.md +35 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +41 -3
- package/dist/cli/commands/commands.candidates.test.js +1 -1
- package/dist/cli/commands/commands.candidates.test.js.map +1 -1
- package/dist/cli/commands/commands.js +9 -15
- package/dist/cli/commands/commands.js.map +1 -1
- package/dist/cli/commands/profiles.test.js +7 -7
- package/dist/cli/commands/profiles.test.js.map +1 -1
- package/dist/cli/discovery/loadManifest.js.map +1 -1
- package/dist/cli/main.js +2 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-K34QTSD5.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-LKZ5DOYW.js} +557 -297
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-O52B4F5S.js} +123 -2338
- package/dist/cli-delegate/cmd_conformance.js +18 -6
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +213 -412
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +3 -3
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +3 -3
- package/dist/hooks/dispatch.js +2588 -1082
- package/dist/install/install.mjs +744 -482
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +159 -15
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- package/dist/scripts/condense.js.map +1 -1
- package/docs/MIGRATION.md +21 -1
- package/docs/contracts/settings-classes.md +41 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +32 -1
- package/src/config/agent-settings.template.yml +34 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +39 -9
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +7 -1
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +52 -5
- package/src/scripts/_lib/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +862 -0
- package/src/scripts/ai_council/clients.ts +182 -3
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- package/src/scripts/audit_command_surface.ts +21 -15
- package/src/scripts/build_catalog_index.ts +1 -1
- package/src/scripts/build_discovery_manifest.ts +30 -34
- package/src/scripts/capture_skill_catalogue.ts +185 -328
- package/src/scripts/check_branch_freshness.ts +217 -62
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/condense.ts +50 -1
- package/src/scripts/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/gen_discovery_baseline.ts +3 -3
- package/src/scripts/hook_manifest.yaml +67 -13
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +17 -4
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_command_tiers.ts +29 -75
- package/src/scripts/lint_originality.ts +101 -14
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_scheduled_deprecations.ts +738 -0
- package/src/scripts/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +95 -1
- package/src/scripts/schemas/command.schema.json +6 -11
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/skill_linter.ts +82 -2
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +113 -8
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
|
@@ -43,9 +43,16 @@
|
|
|
43
43
|
*
|
|
44
44
|
* So the base is resolved, in order: an explicit `--base`, then the base of the
|
|
45
45
|
* OPEN PR for this branch as the forge reports it, then the repo's default
|
|
46
|
-
* branch
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* branch AS THE SERVER REPORTS IT (`ls-remote --symref`), then the local
|
|
47
|
+
* `origin/HEAD`, then `main`. Every message names the base AND where it came
|
|
48
|
+
* from, because "I checked main because that is the PR's base" and "I checked
|
|
49
|
+
* main because I could not ask" must never print the same.
|
|
50
|
+
*
|
|
51
|
+
* The server symref matters for the same reason `remoteHead` exists: the local
|
|
52
|
+
* `refs/remotes/origin/HEAD` is written at clone time and never refreshed, so
|
|
53
|
+
* taking the base NAME from it would reintroduce the stale-local-state failure
|
|
54
|
+
* this module argues against everywhere else. It is kept only as the labelled
|
|
55
|
+
* fallback for when the server cannot be reached.
|
|
49
56
|
*
|
|
50
57
|
* When the forge cannot be asked (no `gh`, not authenticated, no GitHub remote)
|
|
51
58
|
* the run says so out loud even under `--quiet`: it has verified the default
|
|
@@ -59,6 +66,7 @@ import path from "node:path";
|
|
|
59
66
|
import { fileURLToPath } from "node:url";
|
|
60
67
|
|
|
61
68
|
import { runGateCli, runSelfTest, type SelfTestCase } from "./_lib/gate_self_test.js";
|
|
69
|
+
import { workspaceIdentity } from "./_lib/git_common_dir.js";
|
|
62
70
|
import { reportScanned } from "./_lib/scan_scope.js";
|
|
63
71
|
|
|
64
72
|
// ledger-exempt: single remote-ref probe — the entire scope is ONE ls-remote answer (0 or 1 refs) resolved to one aggregate ancestor verdict, and every empty path already publishes its reason via reportScanned allowEmpty; there is no per-target collection to account.
|
|
@@ -66,12 +74,25 @@ const DEFAULT_BASE = "main";
|
|
|
66
74
|
const SELF_TEST_MIN_CASES = 5;
|
|
67
75
|
const SELF_TEST_MIN_REJECT = 2;
|
|
68
76
|
const SCRIPT_REL = "src/scripts/check_branch_freshness.ts";
|
|
69
|
-
/**
|
|
70
|
-
|
|
77
|
+
/**
|
|
78
|
+
* One network round trip on the push path — long enough for a cold `gh`, short
|
|
79
|
+
* enough that the worst case stays under budget.
|
|
80
|
+
*
|
|
81
|
+
* It bounds `git` as well as `gh`, and that is the point rather than tidiness:
|
|
82
|
+
* with a timeout on `gh` only, the two `ls-remote` calls could hang unbounded
|
|
83
|
+
* against an unresponsive SSH remote, so the stated worst case was not one. The
|
|
84
|
+
* slowest path makes three calls (forge, symref, base head), so the arithmetic
|
|
85
|
+
* that keeps this under the 25s `pre_push_budget_seconds` ceiling is 3 x 8s.
|
|
86
|
+
*/
|
|
87
|
+
const NETWORK_TIMEOUT_MS = 8_000;
|
|
71
88
|
|
|
72
89
|
function git(args: string[]): string | null {
|
|
73
90
|
try {
|
|
74
|
-
return execFileSync("git", args, {
|
|
91
|
+
return execFileSync("git", args, {
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
94
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
95
|
+
}).trim();
|
|
75
96
|
} catch {
|
|
76
97
|
return null;
|
|
77
98
|
}
|
|
@@ -89,12 +110,17 @@ export type GhRunner = (args: string[]) => GhResult;
|
|
|
89
110
|
* green it did not earn.
|
|
90
111
|
*/
|
|
91
112
|
export type ForgeAnswer =
|
|
92
|
-
| { kind: "pr"; base: string; number
|
|
113
|
+
| { kind: "pr"; base: string; number?: number }
|
|
93
114
|
| { kind: "none" }
|
|
94
115
|
| { kind: "unavailable"; reason: string };
|
|
95
116
|
|
|
96
117
|
/** Where the base name came from — printed with every verdict. */
|
|
97
|
-
export type BaseSource =
|
|
118
|
+
export type BaseSource =
|
|
119
|
+
| "flag"
|
|
120
|
+
| "pull-request"
|
|
121
|
+
| "origin-symref"
|
|
122
|
+
| "origin-head"
|
|
123
|
+
| "fallback";
|
|
98
124
|
|
|
99
125
|
export interface BaseResolution {
|
|
100
126
|
readonly base: string;
|
|
@@ -109,7 +135,7 @@ function gh(args: string[]): GhResult {
|
|
|
109
135
|
const out = execFileSync("gh", args, {
|
|
110
136
|
encoding: "utf8",
|
|
111
137
|
stdio: ["ignore", "pipe", "ignore"],
|
|
112
|
-
timeout:
|
|
138
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
113
139
|
});
|
|
114
140
|
return { ok: true, stdout: out.toString() };
|
|
115
141
|
} catch (err) {
|
|
@@ -118,7 +144,7 @@ function gh(args: string[]): GhResult {
|
|
|
118
144
|
return { ok: false, reason: "the gh CLI is not installed" };
|
|
119
145
|
}
|
|
120
146
|
if (code === "ETIMEDOUT") {
|
|
121
|
-
return { ok: false, reason: `gh did not answer within ${
|
|
147
|
+
return { ok: false, reason: `gh did not answer within ${NETWORK_TIMEOUT_MS / 1000}s` };
|
|
122
148
|
}
|
|
123
149
|
return { ok: false, reason: "gh could not answer — not authenticated, or no GitHub remote" };
|
|
124
150
|
}
|
|
@@ -153,11 +179,29 @@ export function askForgeForBase(branch: string, run: GhRunner = gh): ForgeAnswer
|
|
|
153
179
|
if (base === "") {
|
|
154
180
|
return { kind: "unavailable", reason: "gh answered without a baseRefName" };
|
|
155
181
|
}
|
|
156
|
-
|
|
182
|
+
// The number is cosmetic — it names the PR in the verdict line — while the
|
|
183
|
+
// base is the answer the gate exists to get. An unusable number therefore
|
|
184
|
+
// does NOT discard the base: doing that made the gate check the repo default
|
|
185
|
+
// instead and warn "could not ask the forge" when it had asked and been
|
|
186
|
+
// answered, trading a correct verdict for a tidier message. `describeBase`
|
|
187
|
+
// omits the number rather than rendering the impossible `#0`.
|
|
188
|
+
const usable =
|
|
189
|
+
typeof row.number === "number" && Number.isInteger(row.number) && row.number > 0;
|
|
190
|
+
return usable ? { kind: "pr", base, number: row.number as number } : { kind: "pr", base };
|
|
157
191
|
}
|
|
158
192
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
193
|
+
/**
|
|
194
|
+
* The default branch as the LOCAL clone believes it — no network.
|
|
195
|
+
*
|
|
196
|
+
* `refs/remotes/origin/HEAD` is written at clone time and never refreshed
|
|
197
|
+
* without `git remote set-head`, so this is exactly the class of stale local
|
|
198
|
+
* state this module argues against everywhere else. It has exactly one caller,
|
|
199
|
+
* `serverDefaultBase`, and is reached only when the server could not be asked —
|
|
200
|
+
* where `describeBase` labels it as possibly stale. It decides no verdict on
|
|
201
|
+
* its own; a short-circuit that let it do so is the regression documented in
|
|
202
|
+
* `resolveBase`.
|
|
203
|
+
*/
|
|
204
|
+
export function localDefaultBase(): { base: string; source: "origin-head" | "fallback" } {
|
|
161
205
|
const ref = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
|
162
206
|
if (ref !== null && ref.startsWith("origin/")) {
|
|
163
207
|
const name = ref.slice("origin/".length).trim();
|
|
@@ -168,24 +212,90 @@ export function repoDefaultBase(): { base: string; source: "origin-head" | "fall
|
|
|
168
212
|
return { base: DEFAULT_BASE, source: "fallback" };
|
|
169
213
|
}
|
|
170
214
|
|
|
215
|
+
/**
|
|
216
|
+
* The default branch as the SERVER reports it right now.
|
|
217
|
+
*
|
|
218
|
+
* Same discipline as `remoteHead`: a gate whose thesis is "ask the remote"
|
|
219
|
+
* must not take the branch NAME from a tracking ref either. Costs one
|
|
220
|
+
* `ls-remote`, the round trip this gate already pays, and degrades to the
|
|
221
|
+
* local ref (labelled as such) when the server cannot be reached.
|
|
222
|
+
*/
|
|
223
|
+
export function serverDefaultBase(): {
|
|
224
|
+
base: string;
|
|
225
|
+
source: "origin-symref" | "origin-head" | "fallback";
|
|
226
|
+
} {
|
|
227
|
+
const out = git(["ls-remote", "--symref", "origin", "HEAD"]);
|
|
228
|
+
if (out !== null) {
|
|
229
|
+
for (const line of out.split("\n")) {
|
|
230
|
+
const trimmed = line.trim();
|
|
231
|
+
if (trimmed.startsWith("ref:")) {
|
|
232
|
+
const name = trimmed.slice("ref:".length).trim().split(/\s+/)[0] ?? "";
|
|
233
|
+
if (name.startsWith("refs/heads/")) {
|
|
234
|
+
const short = name.slice("refs/heads/".length).trim();
|
|
235
|
+
if (short !== "") {
|
|
236
|
+
return { base: short, source: "origin-symref" };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return localDefaultBase();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The base requested on the command line, or null when none was.
|
|
247
|
+
*
|
|
248
|
+
* Both spellings are accepted. `--base=develop` used to be dropped silently by
|
|
249
|
+
* an `indexOf("--base")` lookup, and a dropped request is worse than a rejected
|
|
250
|
+
* one here: the gate then auto-resolves some OTHER base and reports a green
|
|
251
|
+
* about a base the caller never asked about.
|
|
252
|
+
*/
|
|
253
|
+
export function explicitBase(argv: readonly string[]): string | null {
|
|
254
|
+
const joined = argv.find((a) => a.startsWith("--base="));
|
|
255
|
+
if (joined !== undefined) {
|
|
256
|
+
const value = joined.slice("--base=".length);
|
|
257
|
+
return value === "" ? null : value;
|
|
258
|
+
}
|
|
259
|
+
const flag = argv.indexOf("--base");
|
|
260
|
+
if (flag < 0) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const explicit = argv[flag + 1];
|
|
264
|
+
// A bare trailing `--base`, or one followed by another flag, is a typo rather
|
|
265
|
+
// than a base name. Accepting it produced `remoteHead("--quiet")`, which fails
|
|
266
|
+
// and lands on the exit-0 NOT-VERIFIED path — a typo reading as a pass.
|
|
267
|
+
if (explicit === undefined || explicit === "" || explicit.startsWith("-")) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return explicit;
|
|
271
|
+
}
|
|
272
|
+
|
|
171
273
|
/** `--base` wins, then the open PR's base, then the repo default. */
|
|
172
274
|
export function resolveBase(
|
|
173
275
|
argv: readonly string[],
|
|
174
276
|
branch: string,
|
|
175
277
|
forge: (b: string) => ForgeAnswer = (b) => askForgeForBase(b),
|
|
176
278
|
): BaseResolution {
|
|
177
|
-
const
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
if (explicit !== undefined && explicit !== "") {
|
|
181
|
-
return { base: explicit, source: "flag" };
|
|
182
|
-
}
|
|
279
|
+
const explicit = explicitBase(argv);
|
|
280
|
+
if (explicit !== null) {
|
|
281
|
+
return { base: explicit, source: "flag" };
|
|
183
282
|
}
|
|
283
|
+
// There is deliberately no cheap short-circuit for "the branch looks like the
|
|
284
|
+
// default". It was tried and reverted: it decided a VERDICT from
|
|
285
|
+
// `localDefaultBase`, the stale clone-time ref this module argues against, so
|
|
286
|
+
// a branch whose name collided with a stale or fallback local default was
|
|
287
|
+
// skipped without any check at all. The premise it rested on — that
|
|
288
|
+
// `gh pr list --head main` can never usefully answer — is false: a PR from the
|
|
289
|
+
// default branch into a release line or a parent PR is exactly that answer,
|
|
290
|
+
// and it is the case this gate exists for. Standing on the real base is
|
|
291
|
+
// short-circuited one layer up, in `main`, AFTER the base is known.
|
|
184
292
|
const answer = forge(branch);
|
|
185
293
|
if (answer.kind === "pr") {
|
|
186
|
-
return
|
|
294
|
+
return answer.number === undefined
|
|
295
|
+
? { base: answer.base, source: "pull-request" }
|
|
296
|
+
: { base: answer.base, source: "pull-request", pr: answer.number };
|
|
187
297
|
}
|
|
188
|
-
const fallback =
|
|
298
|
+
const fallback = serverDefaultBase();
|
|
189
299
|
return answer.kind === "unavailable"
|
|
190
300
|
? { base: fallback.base, source: fallback.source, unverified: answer.reason }
|
|
191
301
|
: { base: fallback.base, source: fallback.source };
|
|
@@ -197,9 +307,13 @@ export function describeBase(r: BaseResolution): string {
|
|
|
197
307
|
case "flag":
|
|
198
308
|
return "base given on the command line";
|
|
199
309
|
case "pull-request":
|
|
200
|
-
return
|
|
310
|
+
return r.pr === undefined
|
|
311
|
+
? "base of the open PR for this branch"
|
|
312
|
+
: `base of open PR #${r.pr}`;
|
|
313
|
+
case "origin-symref":
|
|
314
|
+
return "repo default branch, as the server reports it";
|
|
201
315
|
case "origin-head":
|
|
202
|
-
return "repo default branch
|
|
316
|
+
return "repo default branch from the LOCAL origin/HEAD ref, which may be stale";
|
|
203
317
|
case "fallback":
|
|
204
318
|
return `repo default (assumed ${DEFAULT_BASE} — origin/HEAD is not set)`;
|
|
205
319
|
}
|
|
@@ -243,7 +357,15 @@ export function containsCommit(sha: string): boolean | null {
|
|
|
243
357
|
* gate reading the tracking ref would pass it — which is exactly the false green
|
|
244
358
|
* this gate exists to remove.
|
|
245
359
|
*/
|
|
246
|
-
|
|
360
|
+
interface Scaffold {
|
|
361
|
+
origin: string;
|
|
362
|
+
work: string;
|
|
363
|
+
g: (args: string[], at: string) => void;
|
|
364
|
+
write: (at: string, name: string, body: string) => void;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The bare origin + working clone every fixture below starts from. */
|
|
368
|
+
function scaffold(root: string): Scaffold {
|
|
247
369
|
const origin = path.join(root, "origin.git");
|
|
248
370
|
const work = path.join(root, "work");
|
|
249
371
|
fs.mkdirSync(origin, { recursive: true });
|
|
@@ -260,6 +382,11 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
|
260
382
|
g(["remote", "add", "origin", origin], work);
|
|
261
383
|
write(work, "a.txt", "one");
|
|
262
384
|
g(["push", "-u", "origin", DEFAULT_BASE], work);
|
|
385
|
+
return { origin, work, g, write };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
389
|
+
const { origin, work, g, write } = scaffold(root);
|
|
263
390
|
g(["checkout", "-b", "feat/x"], work);
|
|
264
391
|
write(work, "b.txt", "two");
|
|
265
392
|
if (opts.advanceRemote) {
|
|
@@ -272,37 +399,31 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
|
272
399
|
}
|
|
273
400
|
|
|
274
401
|
/**
|
|
275
|
-
* A repo where the branch's real base is NOT
|
|
276
|
-
*
|
|
277
|
-
*
|
|
402
|
+
* A repo where the branch's real base is NOT `main`: `feat/x` is cut from
|
|
403
|
+
* `develop`, `develop` then advances on the remote, and `main` never moves.
|
|
404
|
+
* The SERVER's `HEAD` is pointed at `serverDefault`.
|
|
278
405
|
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
406
|
+
* The base is left for the gate to resolve — no `--base`. That is the whole
|
|
407
|
+
* point of the fixture, and the first version got it wrong: it passed
|
|
408
|
+
* `--base` on both cases, which exercises the flag branch that existed before
|
|
409
|
+
* this change. Deleting the entire forge/symref resolution would have left both
|
|
410
|
+
* cases green, so the pair proved nothing about the new behaviour and the raised
|
|
411
|
+
* self-test floors rested on coverage that did not exist. (R2 finding 2.)
|
|
412
|
+
*
|
|
413
|
+
* With `serverDefault = "develop"` the gate must refuse: that is the base this
|
|
414
|
+
* branch is actually behind. With `serverDefault = "main"` the same tree passes.
|
|
415
|
+
* A regression that reinstates a hardcoded `main` flips the first case, because
|
|
416
|
+
* nothing but the resolution decides which base is asked about.
|
|
284
417
|
*/
|
|
285
|
-
function buildTwoBaseFixture(root: string): string {
|
|
286
|
-
const origin =
|
|
287
|
-
const work = path.join(root, "work");
|
|
288
|
-
fs.mkdirSync(origin, { recursive: true });
|
|
289
|
-
const g = (args: string[], at: string): void => {
|
|
290
|
-
execFileSync("git", args, { cwd: at, stdio: "ignore" });
|
|
291
|
-
};
|
|
292
|
-
const write = (at: string, name: string, body: string): void => {
|
|
293
|
-
fs.writeFileSync(path.join(at, name), body);
|
|
294
|
-
g(["add", "-A"], at);
|
|
295
|
-
g(["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", name], at);
|
|
296
|
-
};
|
|
297
|
-
g(["init", "--bare", "-b", DEFAULT_BASE], origin);
|
|
298
|
-
g(["init", "-b", DEFAULT_BASE, work], root);
|
|
299
|
-
g(["remote", "add", "origin", origin], work);
|
|
300
|
-
write(work, "a.txt", "one");
|
|
301
|
-
g(["push", "-u", "origin", DEFAULT_BASE], work);
|
|
418
|
+
function buildTwoBaseFixture(root: string, serverDefault: string): string {
|
|
419
|
+
const { origin, work, g, write } = scaffold(root);
|
|
302
420
|
g(["checkout", "-b", "develop"], work);
|
|
303
421
|
g(["push", "-u", "origin", "develop"], work);
|
|
304
422
|
g(["checkout", "-b", "feat/x"], work);
|
|
305
423
|
write(work, "b.txt", "two");
|
|
424
|
+
// The bare repo's own HEAD is what `ls-remote --symref origin HEAD` reports,
|
|
425
|
+
// so this is the server-side default branch, not a local tracking ref.
|
|
426
|
+
g(["symbolic-ref", "HEAD", `refs/heads/${serverDefault}`], origin);
|
|
306
427
|
// A second clone advances `develop` only — `main` stays exactly where it was.
|
|
307
428
|
const other = path.join(root, "other");
|
|
308
429
|
g(["clone", origin, other], root);
|
|
@@ -312,6 +433,23 @@ function buildTwoBaseFixture(root: string): string {
|
|
|
312
433
|
return work;
|
|
313
434
|
}
|
|
314
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Strip everything that could point the child's `gh` at a real forge.
|
|
438
|
+
*
|
|
439
|
+
* `runGateCli` spawns with the full inherited environment, and since the
|
|
440
|
+
* two-base cases dropped `--base` the resolution itself is what they assert on.
|
|
441
|
+
* An ambient `GH_REPO` can make `gh pr list --head feat/x` answer about a real
|
|
442
|
+
* PR in someone else's repository and silently move the base under test — the
|
|
443
|
+
* same ambient-credential hazard the `forge` seam closed for the unit tests,
|
|
444
|
+
* which would otherwise have stayed live in the only coverage of the new
|
|
445
|
+
* symref resolution. `CI` would no-op every case green.
|
|
446
|
+
*/
|
|
447
|
+
function isolateChildEnv(): void {
|
|
448
|
+
for (const key of ["CI", "GITHUB_ACTIONS", "GH_REPO", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST", "GH_CONFIG_DIR"]) {
|
|
449
|
+
delete process.env[key];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
315
453
|
export function selfTest(repoRoot: string): number {
|
|
316
454
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cbf-selftest-"));
|
|
317
455
|
const run = (advanceRemote: boolean, onBase: boolean): number => {
|
|
@@ -320,17 +458,15 @@ export function selfTest(repoRoot: string): number {
|
|
|
320
458
|
if (onBase) {
|
|
321
459
|
execFileSync("git", ["checkout", DEFAULT_BASE], { cwd: work, stdio: "ignore" });
|
|
322
460
|
}
|
|
323
|
-
|
|
324
|
-
delete process.env["CI"];
|
|
325
|
-
delete process.env["GITHUB_ACTIONS"];
|
|
461
|
+
isolateChildEnv();
|
|
326
462
|
return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
|
|
327
463
|
};
|
|
328
|
-
const runTwoBase = (
|
|
464
|
+
const runTwoBase = (serverDefault: string): number => {
|
|
329
465
|
const dir = fs.mkdtempSync(path.join(root, "twobase-"));
|
|
330
|
-
const work = buildTwoBaseFixture(dir);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"
|
|
466
|
+
const work = buildTwoBaseFixture(dir, serverDefault);
|
|
467
|
+
isolateChildEnv();
|
|
468
|
+
// No `--base`: the gate resolves it, which is the behaviour under test.
|
|
469
|
+
return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
|
|
334
470
|
};
|
|
335
471
|
const cases: SelfTestCase[] = [
|
|
336
472
|
{
|
|
@@ -339,12 +475,12 @@ export function selfTest(repoRoot: string): number {
|
|
|
339
475
|
run: () => run(true, false),
|
|
340
476
|
},
|
|
341
477
|
{
|
|
342
|
-
name: "
|
|
478
|
+
name: "the RESOLVED base decides: server default develop, branch behind it, REFUSED",
|
|
343
479
|
expect: "reject",
|
|
344
480
|
run: () => runTwoBase("develop"),
|
|
345
481
|
},
|
|
346
482
|
{
|
|
347
|
-
name: "
|
|
483
|
+
name: "same tree, server default main — passes, so the resolution is what moved the verdict",
|
|
348
484
|
expect: "accept",
|
|
349
485
|
run: () => runTwoBase(DEFAULT_BASE),
|
|
350
486
|
},
|
|
@@ -389,7 +525,19 @@ function scanReport(scanned: number, allowEmpty?: string): void {
|
|
|
389
525
|
});
|
|
390
526
|
}
|
|
391
527
|
|
|
392
|
-
|
|
528
|
+
/**
|
|
529
|
+
* @param forge Seam for the forge lookup. Production passes nothing and gets the
|
|
530
|
+
* real `gh`. Tests MUST pass a stub: without this parameter the only way to
|
|
531
|
+
* reach the forge-unavailable branch was to not have `gh` installed, so the test
|
|
532
|
+
* for it could only assert conditionally — i.e. it passed whether or not the
|
|
533
|
+
* warning was emitted. It also stopped every fixture test from spawning `gh`
|
|
534
|
+
* against ambient credentials, where a `GH_REPO` override could return a real
|
|
535
|
+
* PR base for a same-named branch and silently change the base under test.
|
|
536
|
+
*/
|
|
537
|
+
export function main(
|
|
538
|
+
argv: string[] = process.argv.slice(2),
|
|
539
|
+
forge?: (b: string) => ForgeAnswer,
|
|
540
|
+
): number {
|
|
393
541
|
if (argv.includes("--self-test")) {
|
|
394
542
|
return selfTest(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
|
395
543
|
}
|
|
@@ -401,14 +549,21 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
401
549
|
return 0;
|
|
402
550
|
}
|
|
403
551
|
|
|
404
|
-
|
|
552
|
+
// Census row B4. The file-based resolver is immune to an inherited `GIT_DIR`
|
|
553
|
+
// (hooks export one, and this gate runs at push time). Behaviour-identical
|
|
554
|
+
// here by construction: the guard below already collapses `null` and the
|
|
555
|
+
// detached-HEAD literal onto the same skip, and unresolved maps to `null`.
|
|
556
|
+
const branchField = workspaceIdentity().branch;
|
|
557
|
+
const branch = branchField.resolved ? branchField.value : null;
|
|
405
558
|
if (branch === null || branch === "HEAD") {
|
|
406
559
|
scanReport(0, "detached HEAD — there is no branch whose freshness could be asked");
|
|
407
560
|
return 0;
|
|
408
561
|
}
|
|
409
562
|
|
|
410
563
|
// Resolved AFTER the branch is known: the PR's base is a fact about THIS branch.
|
|
411
|
-
|
|
564
|
+
// `resolveBase`'s third parameter has a default, so passing `undefined`
|
|
565
|
+
// positionally already selects the real `gh` — no branch needed here.
|
|
566
|
+
const resolved = resolveBase(argv, branch, forge);
|
|
412
567
|
const base = resolved.base;
|
|
413
568
|
|
|
414
569
|
if (branch === base) {
|
|
@@ -26,6 +26,7 @@ import * as fs from 'node:fs';
|
|
|
26
26
|
import * as path from 'node:path';
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
28
|
|
|
29
|
+
import { workspaceIdentity } from './_lib/git_common_dir.js';
|
|
29
30
|
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
30
31
|
|
|
31
32
|
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
|
@@ -65,8 +66,11 @@ function _on_main(): boolean {
|
|
|
65
66
|
if (ref === 'refs/heads/main' || ref === 'refs/heads/master') {
|
|
66
67
|
return true;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// Census row B5. Behaviour-identical: a detached HEAD used to compare the
|
|
70
|
+
// literal `HEAD` against MAIN_BRANCH and fail; unresolved fails the same
|
|
71
|
+
// way, without a subprocess an inherited `GIT_DIR` could redirect.
|
|
72
|
+
const branch = workspaceIdentity().branch;
|
|
73
|
+
return branch.resolved && branch.value === MAIN_BRANCH;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
function _npm_latest(pkg: string): string | null {
|
|
@@ -21,6 +21,7 @@ import * as fs from 'node:fs';
|
|
|
21
21
|
import * as path from 'node:path';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
23
|
|
|
24
|
+
import { workspaceIdentity } from './_lib/git_common_dir.js';
|
|
24
25
|
import { assertScanned } from './_lib/scan_scope.js';
|
|
25
26
|
|
|
26
27
|
const RELEASE_BRANCH_RE = /^release\/(\d+)\.(\d+)\.(\d+)$/;
|
|
@@ -37,8 +38,19 @@ function _git(...args: string[]): string {
|
|
|
37
38
|
return (proc.stdout ?? '').trim();
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Census row B3 — one of four private `_current_branch` implementations that
|
|
43
|
+
* existed beside the shared, file-based one.
|
|
44
|
+
*
|
|
45
|
+
* This one spawned `rev-parse --abbrev-ref HEAD` with **no `cwd` and no env
|
|
46
|
+
* scrub**, so an inherited `GIT_DIR` answered about the hook's repository.
|
|
47
|
+
* The migration is behaviour-identical at the only call site: `main()` treats
|
|
48
|
+
* `'HEAD'` and `''` the same ("detached HEAD — gate skipped"), and the
|
|
49
|
+
* resolver's unresolved case maps onto `''`.
|
|
50
|
+
*/
|
|
40
51
|
function _current_branch(): string {
|
|
41
|
-
|
|
52
|
+
const branch = workspaceIdentity().branch;
|
|
53
|
+
return branch.resolved ? branch.value : '';
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
function _parse_semver(text: string): SemVer | null {
|
|
@@ -148,7 +160,11 @@ function main(): number {
|
|
|
148
160
|
gate: 'check_release_trunk_sync',
|
|
149
161
|
scanned: 0,
|
|
150
162
|
units: 'release-prep branch(es)',
|
|
151
|
-
|
|
163
|
+
// Names what the gate actually reads. It stopped spawning
|
|
164
|
+
// `rev-parse --abbrev-ref HEAD` when B3 moved to the file-based
|
|
165
|
+
// resolver; a scan root naming a command the gate does not run is
|
|
166
|
+
// a false scope declaration.
|
|
167
|
+
roots: ['<git-dir>/HEAD via workspaceIdentity().branch'],
|
|
152
168
|
allowEmpty:
|
|
153
169
|
'OPTIONAL_INPUT: the checked-out branch is not release/X.Y.Z, so there is no ' +
|
|
154
170
|
'target version to compare main against — an absent question, not an empty ' +
|
package/src/scripts/condense.ts
CHANGED
|
@@ -1450,7 +1450,31 @@ export interface ClaudePathsPlan {
|
|
|
1450
1450
|
/** Patterns to emit under `paths:`, in declaration order. */
|
|
1451
1451
|
globs: string[];
|
|
1452
1452
|
/** Patterns deliberately not emitted, with the reason a reader can act on. */
|
|
1453
|
-
dropped: { pattern: string; reason: 'unresolved-placeholder' | 'over-budget' }[];
|
|
1453
|
+
dropped: { pattern: string; reason: 'unresolved-placeholder' | 'over-budget' | 'mixed-triggers' }[];
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Does this rule declare any trigger that is NOT path-shaped?
|
|
1458
|
+
*
|
|
1459
|
+
* Load-bearing for `_claude_paths_plan` below, because one `triggers:` list
|
|
1460
|
+
* means two different things on two hosts. Cursor and Windsurf treat globs as
|
|
1461
|
+
* ADDITIVE — a rule with empty globs still reaches the agent through its
|
|
1462
|
+
* description (see `_emit_cursor_mdc`'s own comment). Claude Code treats
|
|
1463
|
+
* `paths:` as EXCLUSIVE: with it the rule loads on a path match; without it the
|
|
1464
|
+
* rule loads unconditionally. The emitter fed both semantics from one list, so
|
|
1465
|
+
* a rule that declared keywords *and* one path glob silently became
|
|
1466
|
+
* path-gated-only on Claude.
|
|
1467
|
+
*/
|
|
1468
|
+
export function _has_non_path_trigger(meta: Record<string, unknown>): boolean {
|
|
1469
|
+
const triggers = meta['triggers'];
|
|
1470
|
+
if (!Array.isArray(triggers)) return false;
|
|
1471
|
+
for (const t of triggers) {
|
|
1472
|
+
if (t === null || typeof t !== 'object' || Array.isArray(t)) continue;
|
|
1473
|
+
const obj = t as Record<string, unknown>;
|
|
1474
|
+
if (typeof obj['keyword'] === 'string' && obj['keyword']) return true;
|
|
1475
|
+
if (typeof obj['phrase'] === 'string' && obj['phrase']) return true;
|
|
1476
|
+
}
|
|
1477
|
+
return false;
|
|
1454
1478
|
}
|
|
1455
1479
|
|
|
1456
1480
|
/**
|
|
@@ -1470,6 +1494,31 @@ export function _claude_paths_plan(meta: Record<string, unknown>): ClaudePathsPl
|
|
|
1470
1494
|
}
|
|
1471
1495
|
const globs: string[] = [];
|
|
1472
1496
|
const dropped: ClaudePathsPlan['dropped'] = [];
|
|
1497
|
+
|
|
1498
|
+
// A rule that ALSO declares keyword / phrase triggers gets no `paths:` at
|
|
1499
|
+
// all, and therefore keeps loading unconditionally on Claude Code.
|
|
1500
|
+
//
|
|
1501
|
+
// Emitting `paths:` here would narrow the rule to a path match and discard
|
|
1502
|
+
// every keyword the author wrote, because this host reads the list as the
|
|
1503
|
+
// whole gate rather than as one way in. Measured before this guard: 25 of
|
|
1504
|
+
// 110 projected rules carried `paths:` and 19 of them also carried keyword
|
|
1505
|
+
// triggers — `design-fidelity` lost 21 of them, in exactly the pasted-
|
|
1506
|
+
// screenshot and capability-URL handovers its own routing section names as
|
|
1507
|
+
// the classes it exists to catch. Nothing in the tree recorded a decision
|
|
1508
|
+
// to make that trade, which is why it is treated as a defect.
|
|
1509
|
+
//
|
|
1510
|
+
// The drop is REPORTED rather than silent, so a rule whose obligation
|
|
1511
|
+
// really is path-bound is visible as a candidate for removing its keyword
|
|
1512
|
+
// triggers — the narrowing then becomes an authoring decision instead of an
|
|
1513
|
+
// emitter side effect. Cursor and Windsurf are untouched: globs are
|
|
1514
|
+
// additive there, so the same rule keeps both routes.
|
|
1515
|
+
if (_has_non_path_trigger(meta)) {
|
|
1516
|
+
for (const raw of derive_trigger_globs(meta)) {
|
|
1517
|
+
dropped.push({ pattern: raw, reason: 'mixed-triggers' });
|
|
1518
|
+
}
|
|
1519
|
+
return { globs, dropped };
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1473
1522
|
let spent = 0;
|
|
1474
1523
|
for (const raw of derive_trigger_globs(meta)) {
|
|
1475
1524
|
if (_is_unresolved_placeholder(raw)) {
|
|
@@ -222,13 +222,23 @@ export const REVIEW_SCOPE_NAME_ONLY_FLAGS: readonly string[] = [
|
|
|
222
222
|
'-O/dev/null',
|
|
223
223
|
];
|
|
224
224
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
225
|
+
/**
|
|
226
|
+
* `git diff` argv for the review scope (patch body), config-pinned.
|
|
227
|
+
*
|
|
228
|
+
* `head` defaults to `HEAD` — the only value the dispatcher and the validator
|
|
229
|
+
* ever pass, since both bind the scope being dispatched *now*. It is a
|
|
230
|
+
* parameter so that an out-of-band measurement can re-derive the scope at a
|
|
231
|
+
* HISTORICAL revision through this same definition instead of assembling a
|
|
232
|
+
* second `git diff` argv beside it: a private copy would drift from the
|
|
233
|
+
* normative flag list above the first time a flag changes, and then report a
|
|
234
|
+
* hash no gate would ever produce.
|
|
235
|
+
*/
|
|
236
|
+
export function reviewScopeDiffArgs(base: string, head = 'HEAD'): string[] {
|
|
227
237
|
return [
|
|
228
238
|
...REVIEW_SCOPE_GIT_CONFIG,
|
|
229
239
|
'diff',
|
|
230
240
|
...REVIEW_SCOPE_DIFF_FLAGS,
|
|
231
|
-
`${base}
|
|
241
|
+
`${base}...${head}`,
|
|
232
242
|
'--',
|
|
233
243
|
':/',
|
|
234
244
|
...REVIEW_SCOPE_EXCLUDES,
|
|
@@ -236,12 +246,12 @@ export function reviewScopeDiffArgs(base: string): string[] {
|
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
/** `git diff --name-only` argv for the review scope (changed-file list), config-pinned. */
|
|
239
|
-
export function reviewScopeNameOnlyArgs(base: string): string[] {
|
|
249
|
+
export function reviewScopeNameOnlyArgs(base: string, head = 'HEAD'): string[] {
|
|
240
250
|
return [
|
|
241
251
|
...REVIEW_SCOPE_GIT_CONFIG,
|
|
242
252
|
'diff',
|
|
243
253
|
...REVIEW_SCOPE_NAME_ONLY_FLAGS,
|
|
244
|
-
`${base}
|
|
254
|
+
`${base}...${head}`,
|
|
245
255
|
'--',
|
|
246
256
|
':/',
|
|
247
257
|
...REVIEW_SCOPE_EXCLUDES,
|
|
@@ -29,6 +29,8 @@ import * as path from 'node:path';
|
|
|
29
29
|
import process from 'node:process';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
+
import { workspaceIdentity } from './_lib/git_common_dir.js';
|
|
33
|
+
|
|
32
34
|
// argparse `prog` is hardcoded to "evidence_report.py" in the retired Python implementation,
|
|
33
35
|
// so usage strings stay byte-identical regardless of the `.py`/`.ts` filename.
|
|
34
36
|
const PROG = 'evidence_report.py';
|
|
@@ -39,15 +41,24 @@ const DESCRIPTION = 'Evidence Report template automation.';
|
|
|
39
41
|
// Repo-root resolution
|
|
40
42
|
// ---------------------------------------------------------------------------
|
|
41
43
|
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Return repo root, falling back to the relative-path heuristic.
|
|
46
|
+
*
|
|
47
|
+
* Census row R4. The previous implementation spawned
|
|
48
|
+
* `git rev-parse --show-toplevel` with **no `cwd` and no env scrub**, so an
|
|
49
|
+
* inherited `GIT_DIR` (every git hook exports one) silently answered about the
|
|
50
|
+
* hook's repository instead of this one. `workspaceIdentity()` reads files, so
|
|
51
|
+
* there is no child process to redirect. The fallback is unchanged, and in a
|
|
52
|
+
* plain invocation both forms return the same path.
|
|
53
|
+
*/
|
|
43
54
|
function _repo_root(): string {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
55
|
+
const root = workspaceIdentity().repoRoot;
|
|
56
|
+
if (!root.resolved) {
|
|
46
57
|
// Script lives at src/scripts/; repo root is two levels up.
|
|
47
58
|
const here = fileURLToPath(import.meta.url);
|
|
48
59
|
return path.resolve(path.dirname(here), '..', '..');
|
|
49
60
|
}
|
|
50
|
-
return
|
|
61
|
+
return root.value;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
function _git_head_short(): string {
|