@event4u/agent-config 10.4.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +73 -182
- 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 +13 -5
- package/dist/agent-src/commands/council/default.md +26 -12
- 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 +1 -2
- package/dist/agent-src/commands/design-system/capture.md +0 -1
- package/dist/agent-src/commands/design-system/generate.md +23 -2
- 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 +67 -28
- 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 +5 -4
- 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/rules/settings-ask-protocol.md +14 -9
- package/dist/agent-src/skills/ai-council/SKILL.md +24 -6
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +12 -1
- package/dist/agent-src/skills/corpus-grounding/scripts/decision_engine.ts +215 -7
- package/dist/agent-src/skills/corpus-grounding/scripts/ground.ts +39 -1
- package/dist/agent-src/skills/design-intelligence/ATTRIBUTION.md +19 -2
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +36 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -0
- package/dist/agent-src/skills/docx-authoring/SKILL.md +1 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +17 -3
- package/dist/agent-src/skills/pdf-tools/SKILL.md +1 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +35 -0
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +9 -6
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -21
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- 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-delegate/{chunk-BV3WR67D.js → chunk-55WZN5VF.js} +3 -6
- package/dist/cli-delegate/{chunk-X3VOARZD.js → chunk-L565YFXH.js} +3 -3
- package/dist/cli-delegate/{chunk-JMQZ7MM5.js → chunk-MNWRDMR5.js} +1 -1
- package/dist/cli-delegate/{chunk-IR3IVRVC.js → chunk-NLP54U3J.js} +2 -1
- package/dist/cli-delegate/{chunk-FV34EGEX.js → chunk-NN4KW4HZ.js} +1 -1
- package/dist/cli-delegate/{chunk-6ENSXSBW.js → chunk-PPQO4NGR.js} +1 -1
- package/dist/cli-delegate/{chunk-6LOPHJLD.js → chunk-RYNADUFG.js} +1 -1
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +16 -4
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +1 -1
- package/dist/cli-delegate/cmd_settings_get.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +216 -415
- 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 +68 -14
- package/dist/install/install.mjs +5 -7
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +48 -6
- package/dist/scripts/_lib/agent_settings.js +8 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/server/schemas/settings.js +3 -4
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/{index-5fXOHoVh.js → index-B0aMJnuz.js} +2 -2
- package/dist/ui/assets/{index-5fXOHoVh.js.map → index-B0aMJnuz.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +1 -0
- package/docs/contracts/settings-classes.md +53 -9
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +49 -18
- package/src/config/gate-coverage.yml +13 -3
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/pack-size-budget.json +3 -2
- package/src/scripts/_dispatch.bash +42 -5
- package/src/scripts/_lib/agent_settings.ts +8 -0
- 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/check_branch_freshness.ts +393 -12
- package/src/scripts/external_sources_denylist.json +7 -2
- package/src/scripts/gen_discovery_baseline.ts +3 -3
- package/src/scripts/hook_manifest.yaml +20 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +17 -4
- package/src/scripts/lint_command_tiers.ts +29 -75
- package/src/scripts/lint_originality.ts +101 -14
- package/src/scripts/schemas/command.schema.json +4 -9
- package/src/scripts/schemas/skill.schema.json +10 -1
- package/src/scripts/session_register_hook.ts +120 -10
- package/src/scripts/skill_linter.ts +82 -2
- package/src/scripts/worktree_cleanup_check.ts +111 -7
- package/src/server/schemas/settings.ts +3 -6
|
@@ -31,6 +31,33 @@
|
|
|
31
31
|
* It never fetches, merges, or rebases for you. Choosing how to catch up is a
|
|
32
32
|
* git-history decision (`git-history-discipline`), and a gate that rewrites
|
|
33
33
|
* history to make itself pass is a worse failure than the one it reports.
|
|
34
|
+
*
|
|
35
|
+
* WHICH BASE — asked of the forge, not assumed.
|
|
36
|
+
*
|
|
37
|
+
* The first version hardcoded `main`. That is right for most branches here and
|
|
38
|
+
* wrong for every branch whose PR targets something else: a stacked PR, a
|
|
39
|
+
* release line, a consumer repo on `master`. On those the gate produced a false
|
|
40
|
+
* GREEN — "current with origin/main" while the branch sat behind the base it
|
|
41
|
+
* would actually merge into — which is worse than no gate, because it answers a
|
|
42
|
+
* question nobody asked in the voice of the one they did.
|
|
43
|
+
*
|
|
44
|
+
* So the base is resolved, in order: an explicit `--base`, then the base of the
|
|
45
|
+
* OPEN PR for this branch as the forge reports it, then the repo's default
|
|
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.
|
|
56
|
+
*
|
|
57
|
+
* When the forge cannot be asked (no `gh`, not authenticated, no GitHub remote)
|
|
58
|
+
* the run says so out loud even under `--quiet`: it has verified the default
|
|
59
|
+
* base and has NOT ruled out an open PR against a different one. That is a
|
|
60
|
+
* partial answer, and partial answers are reported as partial.
|
|
34
61
|
*/
|
|
35
62
|
import { execFileSync } from "node:child_process";
|
|
36
63
|
import fs from "node:fs";
|
|
@@ -43,18 +70,254 @@ import { reportScanned } from "./_lib/scan_scope.js";
|
|
|
43
70
|
|
|
44
71
|
// 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.
|
|
45
72
|
const DEFAULT_BASE = "main";
|
|
46
|
-
const SELF_TEST_MIN_CASES =
|
|
47
|
-
const SELF_TEST_MIN_REJECT =
|
|
73
|
+
const SELF_TEST_MIN_CASES = 5;
|
|
74
|
+
const SELF_TEST_MIN_REJECT = 2;
|
|
48
75
|
const SCRIPT_REL = "src/scripts/check_branch_freshness.ts";
|
|
76
|
+
/**
|
|
77
|
+
* One network round trip on the push path — long enough for a cold `gh`, short
|
|
78
|
+
* enough that the worst case stays under budget.
|
|
79
|
+
*
|
|
80
|
+
* It bounds `git` as well as `gh`, and that is the point rather than tidiness:
|
|
81
|
+
* with a timeout on `gh` only, the two `ls-remote` calls could hang unbounded
|
|
82
|
+
* against an unresponsive SSH remote, so the stated worst case was not one. The
|
|
83
|
+
* slowest path makes three calls (forge, symref, base head), so the arithmetic
|
|
84
|
+
* that keeps this under the 25s `pre_push_budget_seconds` ceiling is 3 x 8s.
|
|
85
|
+
*/
|
|
86
|
+
const NETWORK_TIMEOUT_MS = 8_000;
|
|
49
87
|
|
|
50
88
|
function git(args: string[]): string | null {
|
|
51
89
|
try {
|
|
52
|
-
return execFileSync("git", args, {
|
|
90
|
+
return execFileSync("git", args, {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
93
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
94
|
+
}).trim();
|
|
53
95
|
} catch {
|
|
54
96
|
return null;
|
|
55
97
|
}
|
|
56
98
|
}
|
|
57
99
|
|
|
100
|
+
export type GhResult = { ok: true; stdout: string } | { ok: false; reason: string };
|
|
101
|
+
export type GhRunner = (args: string[]) => GhResult;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* What the forge said about an open PR for this branch.
|
|
105
|
+
*
|
|
106
|
+
* Three-valued on purpose. `none` (the forge answered: no open PR) and
|
|
107
|
+
* `unavailable` (the forge could not be asked) lead to the same base and to
|
|
108
|
+
* DIFFERENT confidence, and collapsing them is exactly how a gate reports a
|
|
109
|
+
* green it did not earn.
|
|
110
|
+
*/
|
|
111
|
+
export type ForgeAnswer =
|
|
112
|
+
| { kind: "pr"; base: string; number?: number }
|
|
113
|
+
| { kind: "none" }
|
|
114
|
+
| { kind: "unavailable"; reason: string };
|
|
115
|
+
|
|
116
|
+
/** Where the base name came from — printed with every verdict. */
|
|
117
|
+
export type BaseSource =
|
|
118
|
+
| "flag"
|
|
119
|
+
| "pull-request"
|
|
120
|
+
| "origin-symref"
|
|
121
|
+
| "origin-head"
|
|
122
|
+
| "fallback";
|
|
123
|
+
|
|
124
|
+
export interface BaseResolution {
|
|
125
|
+
readonly base: string;
|
|
126
|
+
readonly source: BaseSource;
|
|
127
|
+
readonly pr?: number;
|
|
128
|
+
/** Present iff an open PR against a different base could not be ruled out. */
|
|
129
|
+
readonly unverified?: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function gh(args: string[]): GhResult {
|
|
133
|
+
try {
|
|
134
|
+
const out = execFileSync("gh", args, {
|
|
135
|
+
encoding: "utf8",
|
|
136
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
137
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
138
|
+
});
|
|
139
|
+
return { ok: true, stdout: out.toString() };
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
142
|
+
if (code === "ENOENT") {
|
|
143
|
+
return { ok: false, reason: "the gh CLI is not installed" };
|
|
144
|
+
}
|
|
145
|
+
if (code === "ETIMEDOUT") {
|
|
146
|
+
return { ok: false, reason: `gh did not answer within ${NETWORK_TIMEOUT_MS / 1000}s` };
|
|
147
|
+
}
|
|
148
|
+
return { ok: false, reason: "gh could not answer — not authenticated, or no GitHub remote" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The base of the open PR whose head is `branch`.
|
|
154
|
+
*
|
|
155
|
+
* `pr list --head` rather than `pr view`: `pr view` will happily resolve a
|
|
156
|
+
* CLOSED PR for the same branch, and a merged PR's base is not the base the
|
|
157
|
+
* next push has to be current with.
|
|
158
|
+
*/
|
|
159
|
+
export function askForgeForBase(branch: string, run: GhRunner = gh): ForgeAnswer {
|
|
160
|
+
const res = run([
|
|
161
|
+
"pr", "list", "--head", branch, "--state", "open",
|
|
162
|
+
"--json", "number,baseRefName", "--limit", "1",
|
|
163
|
+
]);
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
return { kind: "unavailable", reason: res.reason };
|
|
166
|
+
}
|
|
167
|
+
let rows: unknown;
|
|
168
|
+
try {
|
|
169
|
+
rows = JSON.parse(res.stdout);
|
|
170
|
+
} catch {
|
|
171
|
+
return { kind: "unavailable", reason: "gh returned output that is not JSON" };
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
174
|
+
return { kind: "none" };
|
|
175
|
+
}
|
|
176
|
+
const row = rows[0] as { baseRefName?: unknown; number?: unknown };
|
|
177
|
+
const base = typeof row.baseRefName === "string" ? row.baseRefName.trim() : "";
|
|
178
|
+
if (base === "") {
|
|
179
|
+
return { kind: "unavailable", reason: "gh answered without a baseRefName" };
|
|
180
|
+
}
|
|
181
|
+
// The number is cosmetic — it names the PR in the verdict line — while the
|
|
182
|
+
// base is the answer the gate exists to get. An unusable number therefore
|
|
183
|
+
// does NOT discard the base: doing that made the gate check the repo default
|
|
184
|
+
// instead and warn "could not ask the forge" when it had asked and been
|
|
185
|
+
// answered, trading a correct verdict for a tidier message. `describeBase`
|
|
186
|
+
// omits the number rather than rendering the impossible `#0`.
|
|
187
|
+
const usable =
|
|
188
|
+
typeof row.number === "number" && Number.isInteger(row.number) && row.number > 0;
|
|
189
|
+
return usable ? { kind: "pr", base, number: row.number as number } : { kind: "pr", base };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The default branch as the LOCAL clone believes it — no network.
|
|
194
|
+
*
|
|
195
|
+
* `refs/remotes/origin/HEAD` is written at clone time and never refreshed
|
|
196
|
+
* without `git remote set-head`, so this is exactly the class of stale local
|
|
197
|
+
* state this module argues against everywhere else. It has exactly one caller,
|
|
198
|
+
* `serverDefaultBase`, and is reached only when the server could not be asked —
|
|
199
|
+
* where `describeBase` labels it as possibly stale. It decides no verdict on
|
|
200
|
+
* its own; a short-circuit that let it do so is the regression documented in
|
|
201
|
+
* `resolveBase`.
|
|
202
|
+
*/
|
|
203
|
+
export function localDefaultBase(): { base: string; source: "origin-head" | "fallback" } {
|
|
204
|
+
const ref = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
|
|
205
|
+
if (ref !== null && ref.startsWith("origin/")) {
|
|
206
|
+
const name = ref.slice("origin/".length).trim();
|
|
207
|
+
if (name !== "") {
|
|
208
|
+
return { base: name, source: "origin-head" };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { base: DEFAULT_BASE, source: "fallback" };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The default branch as the SERVER reports it right now.
|
|
216
|
+
*
|
|
217
|
+
* Same discipline as `remoteHead`: a gate whose thesis is "ask the remote"
|
|
218
|
+
* must not take the branch NAME from a tracking ref either. Costs one
|
|
219
|
+
* `ls-remote`, the round trip this gate already pays, and degrades to the
|
|
220
|
+
* local ref (labelled as such) when the server cannot be reached.
|
|
221
|
+
*/
|
|
222
|
+
export function serverDefaultBase(): {
|
|
223
|
+
base: string;
|
|
224
|
+
source: "origin-symref" | "origin-head" | "fallback";
|
|
225
|
+
} {
|
|
226
|
+
const out = git(["ls-remote", "--symref", "origin", "HEAD"]);
|
|
227
|
+
if (out !== null) {
|
|
228
|
+
for (const line of out.split("\n")) {
|
|
229
|
+
const trimmed = line.trim();
|
|
230
|
+
if (trimmed.startsWith("ref:")) {
|
|
231
|
+
const name = trimmed.slice("ref:".length).trim().split(/\s+/)[0] ?? "";
|
|
232
|
+
if (name.startsWith("refs/heads/")) {
|
|
233
|
+
const short = name.slice("refs/heads/".length).trim();
|
|
234
|
+
if (short !== "") {
|
|
235
|
+
return { base: short, source: "origin-symref" };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return localDefaultBase();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The base requested on the command line, or null when none was.
|
|
246
|
+
*
|
|
247
|
+
* Both spellings are accepted. `--base=develop` used to be dropped silently by
|
|
248
|
+
* an `indexOf("--base")` lookup, and a dropped request is worse than a rejected
|
|
249
|
+
* one here: the gate then auto-resolves some OTHER base and reports a green
|
|
250
|
+
* about a base the caller never asked about.
|
|
251
|
+
*/
|
|
252
|
+
export function explicitBase(argv: readonly string[]): string | null {
|
|
253
|
+
const joined = argv.find((a) => a.startsWith("--base="));
|
|
254
|
+
if (joined !== undefined) {
|
|
255
|
+
const value = joined.slice("--base=".length);
|
|
256
|
+
return value === "" ? null : value;
|
|
257
|
+
}
|
|
258
|
+
const flag = argv.indexOf("--base");
|
|
259
|
+
if (flag < 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const explicit = argv[flag + 1];
|
|
263
|
+
// A bare trailing `--base`, or one followed by another flag, is a typo rather
|
|
264
|
+
// than a base name. Accepting it produced `remoteHead("--quiet")`, which fails
|
|
265
|
+
// and lands on the exit-0 NOT-VERIFIED path — a typo reading as a pass.
|
|
266
|
+
if (explicit === undefined || explicit === "" || explicit.startsWith("-")) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
return explicit;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** `--base` wins, then the open PR's base, then the repo default. */
|
|
273
|
+
export function resolveBase(
|
|
274
|
+
argv: readonly string[],
|
|
275
|
+
branch: string,
|
|
276
|
+
forge: (b: string) => ForgeAnswer = (b) => askForgeForBase(b),
|
|
277
|
+
): BaseResolution {
|
|
278
|
+
const explicit = explicitBase(argv);
|
|
279
|
+
if (explicit !== null) {
|
|
280
|
+
return { base: explicit, source: "flag" };
|
|
281
|
+
}
|
|
282
|
+
// There is deliberately no cheap short-circuit for "the branch looks like the
|
|
283
|
+
// default". It was tried and reverted: it decided a VERDICT from
|
|
284
|
+
// `localDefaultBase`, the stale clone-time ref this module argues against, so
|
|
285
|
+
// a branch whose name collided with a stale or fallback local default was
|
|
286
|
+
// skipped without any check at all. The premise it rested on — that
|
|
287
|
+
// `gh pr list --head main` can never usefully answer — is false: a PR from the
|
|
288
|
+
// default branch into a release line or a parent PR is exactly that answer,
|
|
289
|
+
// and it is the case this gate exists for. Standing on the real base is
|
|
290
|
+
// short-circuited one layer up, in `main`, AFTER the base is known.
|
|
291
|
+
const answer = forge(branch);
|
|
292
|
+
if (answer.kind === "pr") {
|
|
293
|
+
return answer.number === undefined
|
|
294
|
+
? { base: answer.base, source: "pull-request" }
|
|
295
|
+
: { base: answer.base, source: "pull-request", pr: answer.number };
|
|
296
|
+
}
|
|
297
|
+
const fallback = serverDefaultBase();
|
|
298
|
+
return answer.kind === "unavailable"
|
|
299
|
+
? { base: fallback.base, source: fallback.source, unverified: answer.reason }
|
|
300
|
+
: { base: fallback.base, source: fallback.source };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Human-readable provenance — never omitted, so no verdict is ambiguous about what it checked. */
|
|
304
|
+
export function describeBase(r: BaseResolution): string {
|
|
305
|
+
switch (r.source) {
|
|
306
|
+
case "flag":
|
|
307
|
+
return "base given on the command line";
|
|
308
|
+
case "pull-request":
|
|
309
|
+
return r.pr === undefined
|
|
310
|
+
? "base of the open PR for this branch"
|
|
311
|
+
: `base of open PR #${r.pr}`;
|
|
312
|
+
case "origin-symref":
|
|
313
|
+
return "repo default branch, as the server reports it";
|
|
314
|
+
case "origin-head":
|
|
315
|
+
return "repo default branch from the LOCAL origin/HEAD ref, which may be stale";
|
|
316
|
+
case "fallback":
|
|
317
|
+
return `repo default (assumed ${DEFAULT_BASE} — origin/HEAD is not set)`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
58
321
|
/** The SHA the server reports for `<base>` right now — never a tracking ref. */
|
|
59
322
|
export function remoteHead(base: string, remote = "origin"): string | null {
|
|
60
323
|
const out = git(["ls-remote", "--heads", remote, base]);
|
|
@@ -93,7 +356,15 @@ export function containsCommit(sha: string): boolean | null {
|
|
|
93
356
|
* gate reading the tracking ref would pass it — which is exactly the false green
|
|
94
357
|
* this gate exists to remove.
|
|
95
358
|
*/
|
|
96
|
-
|
|
359
|
+
interface Scaffold {
|
|
360
|
+
origin: string;
|
|
361
|
+
work: string;
|
|
362
|
+
g: (args: string[], at: string) => void;
|
|
363
|
+
write: (at: string, name: string, body: string) => void;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The bare origin + working clone every fixture below starts from. */
|
|
367
|
+
function scaffold(root: string): Scaffold {
|
|
97
368
|
const origin = path.join(root, "origin.git");
|
|
98
369
|
const work = path.join(root, "work");
|
|
99
370
|
fs.mkdirSync(origin, { recursive: true });
|
|
@@ -110,6 +381,11 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
|
110
381
|
g(["remote", "add", "origin", origin], work);
|
|
111
382
|
write(work, "a.txt", "one");
|
|
112
383
|
g(["push", "-u", "origin", DEFAULT_BASE], work);
|
|
384
|
+
return { origin, work, g, write };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
388
|
+
const { origin, work, g, write } = scaffold(root);
|
|
113
389
|
g(["checkout", "-b", "feat/x"], work);
|
|
114
390
|
write(work, "b.txt", "two");
|
|
115
391
|
if (opts.advanceRemote) {
|
|
@@ -121,6 +397,58 @@ function buildFixture(root: string, opts: { advanceRemote: boolean }): string {
|
|
|
121
397
|
return work;
|
|
122
398
|
}
|
|
123
399
|
|
|
400
|
+
/**
|
|
401
|
+
* A repo where the branch's real base is NOT `main`: `feat/x` is cut from
|
|
402
|
+
* `develop`, `develop` then advances on the remote, and `main` never moves.
|
|
403
|
+
* The SERVER's `HEAD` is pointed at `serverDefault`.
|
|
404
|
+
*
|
|
405
|
+
* The base is left for the gate to resolve — no `--base`. That is the whole
|
|
406
|
+
* point of the fixture, and the first version got it wrong: it passed
|
|
407
|
+
* `--base` on both cases, which exercises the flag branch that existed before
|
|
408
|
+
* this change. Deleting the entire forge/symref resolution would have left both
|
|
409
|
+
* cases green, so the pair proved nothing about the new behaviour and the raised
|
|
410
|
+
* self-test floors rested on coverage that did not exist. (R2 finding 2.)
|
|
411
|
+
*
|
|
412
|
+
* With `serverDefault = "develop"` the gate must refuse: that is the base this
|
|
413
|
+
* branch is actually behind. With `serverDefault = "main"` the same tree passes.
|
|
414
|
+
* A regression that reinstates a hardcoded `main` flips the first case, because
|
|
415
|
+
* nothing but the resolution decides which base is asked about.
|
|
416
|
+
*/
|
|
417
|
+
function buildTwoBaseFixture(root: string, serverDefault: string): string {
|
|
418
|
+
const { origin, work, g, write } = scaffold(root);
|
|
419
|
+
g(["checkout", "-b", "develop"], work);
|
|
420
|
+
g(["push", "-u", "origin", "develop"], work);
|
|
421
|
+
g(["checkout", "-b", "feat/x"], work);
|
|
422
|
+
write(work, "b.txt", "two");
|
|
423
|
+
// The bare repo's own HEAD is what `ls-remote --symref origin HEAD` reports,
|
|
424
|
+
// so this is the server-side default branch, not a local tracking ref.
|
|
425
|
+
g(["symbolic-ref", "HEAD", `refs/heads/${serverDefault}`], origin);
|
|
426
|
+
// A second clone advances `develop` only — `main` stays exactly where it was.
|
|
427
|
+
const other = path.join(root, "other");
|
|
428
|
+
g(["clone", origin, other], root);
|
|
429
|
+
g(["checkout", "develop"], other);
|
|
430
|
+
write(other, "c.txt", "three");
|
|
431
|
+
g(["push"], other);
|
|
432
|
+
return work;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Strip everything that could point the child's `gh` at a real forge.
|
|
437
|
+
*
|
|
438
|
+
* `runGateCli` spawns with the full inherited environment, and since the
|
|
439
|
+
* two-base cases dropped `--base` the resolution itself is what they assert on.
|
|
440
|
+
* An ambient `GH_REPO` can make `gh pr list --head feat/x` answer about a real
|
|
441
|
+
* PR in someone else's repository and silently move the base under test — the
|
|
442
|
+
* same ambient-credential hazard the `forge` seam closed for the unit tests,
|
|
443
|
+
* which would otherwise have stayed live in the only coverage of the new
|
|
444
|
+
* symref resolution. `CI` would no-op every case green.
|
|
445
|
+
*/
|
|
446
|
+
function isolateChildEnv(): void {
|
|
447
|
+
for (const key of ["CI", "GITHUB_ACTIONS", "GH_REPO", "GH_TOKEN", "GITHUB_TOKEN", "GH_HOST", "GH_CONFIG_DIR"]) {
|
|
448
|
+
delete process.env[key];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
124
452
|
export function selfTest(repoRoot: string): number {
|
|
125
453
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cbf-selftest-"));
|
|
126
454
|
const run = (advanceRemote: boolean, onBase: boolean): number => {
|
|
@@ -129,9 +457,14 @@ export function selfTest(repoRoot: string): number {
|
|
|
129
457
|
if (onBase) {
|
|
130
458
|
execFileSync("git", ["checkout", DEFAULT_BASE], { cwd: work, stdio: "ignore" });
|
|
131
459
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
460
|
+
isolateChildEnv();
|
|
461
|
+
return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
|
|
462
|
+
};
|
|
463
|
+
const runTwoBase = (serverDefault: string): number => {
|
|
464
|
+
const dir = fs.mkdtempSync(path.join(root, "twobase-"));
|
|
465
|
+
const work = buildTwoBaseFixture(dir, serverDefault);
|
|
466
|
+
isolateChildEnv();
|
|
467
|
+
// No `--base`: the gate resolves it, which is the behaviour under test.
|
|
135
468
|
return runGateCli(repoRoot, SCRIPT_REL, ["--quiet"], work);
|
|
136
469
|
};
|
|
137
470
|
const cases: SelfTestCase[] = [
|
|
@@ -140,6 +473,16 @@ export function selfTest(repoRoot: string): number {
|
|
|
140
473
|
expect: "reject",
|
|
141
474
|
run: () => run(true, false),
|
|
142
475
|
},
|
|
476
|
+
{
|
|
477
|
+
name: "the RESOLVED base decides: server default develop, branch behind it, REFUSED",
|
|
478
|
+
expect: "reject",
|
|
479
|
+
run: () => runTwoBase("develop"),
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "same tree, server default main — passes, so the resolution is what moved the verdict",
|
|
483
|
+
expect: "accept",
|
|
484
|
+
run: () => runTwoBase(DEFAULT_BASE),
|
|
485
|
+
},
|
|
143
486
|
{
|
|
144
487
|
name: "a branch containing the remote base passes",
|
|
145
488
|
expect: "accept",
|
|
@@ -181,13 +524,23 @@ function scanReport(scanned: number, allowEmpty?: string): void {
|
|
|
181
524
|
});
|
|
182
525
|
}
|
|
183
526
|
|
|
184
|
-
|
|
527
|
+
/**
|
|
528
|
+
* @param forge Seam for the forge lookup. Production passes nothing and gets the
|
|
529
|
+
* real `gh`. Tests MUST pass a stub: without this parameter the only way to
|
|
530
|
+
* reach the forge-unavailable branch was to not have `gh` installed, so the test
|
|
531
|
+
* for it could only assert conditionally — i.e. it passed whether or not the
|
|
532
|
+
* warning was emitted. It also stopped every fixture test from spawning `gh`
|
|
533
|
+
* against ambient credentials, where a `GH_REPO` override could return a real
|
|
534
|
+
* PR base for a same-named branch and silently change the base under test.
|
|
535
|
+
*/
|
|
536
|
+
export function main(
|
|
537
|
+
argv: string[] = process.argv.slice(2),
|
|
538
|
+
forge?: (b: string) => ForgeAnswer,
|
|
539
|
+
): number {
|
|
185
540
|
if (argv.includes("--self-test")) {
|
|
186
541
|
return selfTest(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
|
187
542
|
}
|
|
188
543
|
const quiet = argv.includes("--quiet");
|
|
189
|
-
const baseArg = argv.indexOf("--base");
|
|
190
|
-
const base = baseArg >= 0 ? (argv[baseArg + 1] ?? DEFAULT_BASE) : DEFAULT_BASE;
|
|
191
544
|
|
|
192
545
|
// CI merges or refuses on the server; a second opinion here would be noise.
|
|
193
546
|
if (process.env["CI"] === "true" || process.env["GITHUB_ACTIONS"] === "true") {
|
|
@@ -200,11 +553,29 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
200
553
|
scanReport(0, "detached HEAD — there is no branch whose freshness could be asked");
|
|
201
554
|
return 0;
|
|
202
555
|
}
|
|
556
|
+
|
|
557
|
+
// Resolved AFTER the branch is known: the PR's base is a fact about THIS branch.
|
|
558
|
+
// `resolveBase`'s third parameter has a default, so passing `undefined`
|
|
559
|
+
// positionally already selects the real `gh` — no branch needed here.
|
|
560
|
+
const resolved = resolveBase(argv, branch, forge);
|
|
561
|
+
const base = resolved.base;
|
|
562
|
+
|
|
203
563
|
if (branch === base) {
|
|
204
564
|
scanReport(0, `standing on ${base} itself — a branch cannot be behind itself`);
|
|
205
565
|
return 0;
|
|
206
566
|
}
|
|
207
567
|
|
|
568
|
+
// Loud even under --quiet: this run verified the default base and did NOT
|
|
569
|
+
// rule out an open PR against a different one. A partial answer is reported
|
|
570
|
+
// as partial, never folded into the plain green below.
|
|
571
|
+
if (resolved.unverified !== undefined) {
|
|
572
|
+
process.stdout.write(
|
|
573
|
+
`⚠️ check_branch_freshness: could not ask the forge for this branch's PR base ` +
|
|
574
|
+
`(${resolved.unverified}) — checking origin/${base}, the ${describeBase(resolved)}. ` +
|
|
575
|
+
"If an open PR targets a different base, THAT base is NOT verified here.\n",
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
208
579
|
const sha = remoteHead(base);
|
|
209
580
|
if (sha === null) {
|
|
210
581
|
// Loud, not silent. An offline run has not verified the invariant, and a
|
|
@@ -221,19 +592,29 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
221
592
|
scanReport(1);
|
|
222
593
|
if (containsCommit(sha) === true) {
|
|
223
594
|
if (!quiet) {
|
|
224
|
-
process.stdout.write(
|
|
595
|
+
process.stdout.write(
|
|
596
|
+
`✅ branch is current with origin/${base} (${sha.slice(0, 9)}) — ${describeBase(resolved)}\n`,
|
|
597
|
+
);
|
|
225
598
|
}
|
|
226
599
|
return 0;
|
|
227
600
|
}
|
|
228
601
|
|
|
229
|
-
console.error(
|
|
602
|
+
console.error(
|
|
603
|
+
`❌ branch is BEHIND origin/${base} — the remote is at ${sha.slice(0, 9)}. ` +
|
|
604
|
+
`(${describeBase(resolved)})`,
|
|
605
|
+
);
|
|
230
606
|
console.error("");
|
|
231
607
|
console.error(" Pushing now opens a PR that may conflict, and worse: another branch");
|
|
232
608
|
console.error(" may already have shipped what this one is implementing.");
|
|
233
609
|
console.error("");
|
|
234
610
|
console.error(` git fetch origin && git merge origin/${base}`);
|
|
611
|
+
console.error(" ./agent-config roadmap:progress # regenerate AFTER every merge, not only on conflict");
|
|
235
612
|
console.error(" then re-run the gates before pushing.");
|
|
236
613
|
console.error("");
|
|
614
|
+
console.error(" The dashboard line is not optional housekeeping: the roadmap dashboard is a");
|
|
615
|
+
console.error(" GENERATED file, so a merge that lands cleanly still leaves it describing");
|
|
616
|
+
console.error(" neither side's roadmap set. A clean auto-merge of a generated file is still wrong.");
|
|
617
|
+
console.error("");
|
|
237
618
|
console.error(
|
|
238
619
|
" This gate asks the REMOTE, not your tracking ref — a fetch from earlier in",
|
|
239
620
|
);
|
|
@@ -95,7 +95,10 @@
|
|
|
95
95
|
"agents/roadmaps/archive/road-to-subagent-value-realization.md",
|
|
96
96
|
"agents/roadmap-assets/road-to-image-brand-typography.assets.md",
|
|
97
97
|
"CREDITS.md",
|
|
98
|
-
"agents/roadmaps/archive/road-to-ecosystem-harvest-index.md"
|
|
98
|
+
"agents/roadmaps/archive/road-to-ecosystem-harvest-index.md",
|
|
99
|
+
"provenance/borrows.jsonl",
|
|
100
|
+
"docs/THIRD-PARTY-NOTICES.md",
|
|
101
|
+
"agents/evidence/reviews/*.review-input/diff.patch"
|
|
99
102
|
],
|
|
100
103
|
"skip_reason": {
|
|
101
104
|
"vendored_cluster": "design-intelligence/corpus-grounding/design-tokens/react-shadcn-ui/tailwind-engineer + cost/*.mjs ship vendored Apache/MIT code; their upstream attribution is license-required and must NOT be stripped (ADR-061). CREDITS.md is the repo-level attribution file FOR that same cluster — it names the upstreams because the licenses require it, which is the one case source-confidentiality explicitly exempts. It was missing here, so the gate flagged the very attribution the rule mandates.",
|
|
@@ -103,7 +106,9 @@
|
|
|
103
106
|
"generated_bundles": "content.json* bundle the vendored-cluster skill bodies above.",
|
|
104
107
|
"self": "the linter + its denylist + the source-confidentiality rule necessarily contain the deny tokens as data.",
|
|
105
108
|
"validate_safe_paths": "telegraph-derived (MIT) ported code; upstream attribution is license-required.",
|
|
106
|
-
"archived_roadmaps_newly_tracked": "Archived roadmaps (and their assets) that predate the source-confidentiality sweep became tracked via the 2026-07 agents-dir gitignore hygiene change. They are frozen historical records; the claudekit mentions are ADR-061 license-attribution work and the remaining names are historical planning references. New roadmaps stay subject to the gate. road-to-ecosystem-harvest-index.md is the sharpest case: the flagged line is its own acceptance criterion, 'No harvest-family artifact names a real external source' — it quotes the names in order to assert their absence, so the gate fires on the sentence that records the cleanup. road-to-opt-retrieval-and-memory.md was REMOVED from this list on 2026-07-28 (road-to-native-code-intelligence Phase 6): its two clear-text mentions named a gitignored borrow-catalogue inbox file, which carries no historical value that a neutral descriptor loses — so the mentions were rewritten and the exception retired rather than kept. Retiring an exception is preferred over holding one whenever the named token is not license-required attribution and not load-bearing for the record."
|
|
109
|
+
"archived_roadmaps_newly_tracked": "Archived roadmaps (and their assets) that predate the source-confidentiality sweep became tracked via the 2026-07 agents-dir gitignore hygiene change. They are frozen historical records; the claudekit mentions are ADR-061 license-attribution work and the remaining names are historical planning references. New roadmaps stay subject to the gate. road-to-ecosystem-harvest-index.md is the sharpest case: the flagged line is its own acceptance criterion, 'No harvest-family artifact names a real external source' — it quotes the names in order to assert their absence, so the gate fires on the sentence that records the cleanup. road-to-opt-retrieval-and-memory.md was REMOVED from this list on 2026-07-28 (road-to-native-code-intelligence Phase 6): its two clear-text mentions named a gitignored borrow-catalogue inbox file, which carries no historical value that a neutral descriptor loses — so the mentions were rewritten and the exception retired rather than kept. Retiring an exception is preferred over holding one whenever the named token is not license-required attribution and not load-bearing for the record.",
|
|
110
|
+
"provenance_ledger": "provenance/borrows.jsonl and the docs/THIRD-PARTY-NOTICES.md it generates are the license-attribution surfaces for CODE borrows, and code-provenance REQUIRES the upstream name in them: the ledger's `source_url` is a mandatory field (provenance/README.md), and lint_provenance fails when the notices file drifts from it. That is the same license-required-attribution case source-confidentiality already exempts for the vendored cluster and CREDITS.md — the rule forbids derivation-attribution, not the notice a license obliges. The collision only surfaced on 2026-08-13 because the ledger had been empty until its first record; before that there was no name to flag. Scope is deliberately these two paths and not `provenance/*`: the harvest ledger next door governs KNOWLEDGE claims, where anonymisation is the rule and no license compels a name.",
|
|
111
|
+
"r2_review_diff_capture": "The R2 completion-review dispatcher writes a verbatim `diff.patch` of the branch under review into a tracked input package. It is a MECHANICAL CAPTURE, not authored text: any source name inside it was already written somewhere else in the same branch, where this gate does see it. A branch that touches the vendored cluster's ATTRIBUTION.md — carved out here precisely because its license requires the name — therefore reproduces that name in the capture and fails the gate for content the gate already allowed at its source. Scope is the capture ONLY: `findings.md` and `prompt.md` are authored prose and stay under the gate, so a reviewer who names a harvest source in their findings is still caught."
|
|
107
112
|
},
|
|
108
113
|
"_excluded_tokens": {
|
|
109
114
|
"agent-os": "collides with our own coined 'Agent-OS primitive' term (universal-skills) — too FP-prone to auto-deny; scrub external 'agent-os' mentions by review.",
|
|
@@ -247,19 +247,19 @@ export function main(): number {
|
|
|
247
247
|
out.push('');
|
|
248
248
|
|
|
249
249
|
out.push('## Sample commands (first 10)\n');
|
|
250
|
-
out.push('| File | name |
|
|
250
|
+
out.push('| File | name | visibility | cluster |');
|
|
251
251
|
out.push('|---|---|---|---|');
|
|
252
252
|
for (const c of commands.slice(0, 10)) {
|
|
253
253
|
const fm = _frontmatter(c);
|
|
254
254
|
const rel = _relativeTo(c, SRC);
|
|
255
|
-
out.push(`| \`${rel}\` | ${fm.name ?? ''} | ${fm.
|
|
255
|
+
out.push(`| \`${rel}\` | ${fm.name ?? ''} | ${fm.visibility ?? ''} | ${fm.cluster ?? ''} |`);
|
|
256
256
|
}
|
|
257
257
|
out.push('');
|
|
258
258
|
|
|
259
259
|
out.push('## Notes\n');
|
|
260
260
|
out.push('- Every skill carries `domain:` today — Phase 4 mapping uses it as primary signal.');
|
|
261
261
|
out.push('- Rules use `type` (always | auto) + `tier` (safety-floor | kernel | …); no `domain:` today.');
|
|
262
|
-
out.push('- Commands use `
|
|
262
|
+
out.push('- Commands use `visibility` + `cluster`; no `domain:` today.');
|
|
263
263
|
out.push('- Templates have no consistent frontmatter; Phase 4 quarantines them in `unassigned-artefacts.yml`.');
|
|
264
264
|
out.push('');
|
|
265
265
|
|
|
@@ -590,6 +590,26 @@ concerns:
|
|
|
590
590
|
# blocker: in-process Agent-tool subagents inherit the host process env and
|
|
591
591
|
# cannot be marked per-spawn; the axis binds where the suite launches a CLI
|
|
592
592
|
# session itself.
|
|
593
|
+
#
|
|
594
|
+
# Superseded in part, 2026-08-14 (road-to-subagent-lifecycle-integrity Phase 4
|
|
595
|
+
# Step 2). The env half above still stands — nothing marks an in-process spawn
|
|
596
|
+
# per-process. What has changed is that env is no longer the only candidate
|
|
597
|
+
# channel: `subagent_start` / `subagent_stop` are bound on this host
|
|
598
|
+
# (2.1.229, Phase 0 Step 1) and their payload carries `agent_id`, which
|
|
599
|
+
# `subagent-ledger` correlates on today. So a payload-bound role axis —
|
|
600
|
+
# `agent_id` present ⇒ role `worker` — is a live proposal (Phase 4 Step 1),
|
|
601
|
+
# not a closed question.
|
|
602
|
+
# It is NOT implemented, and the reason is a missing measurement rather than a
|
|
603
|
+
# missing decision: whether `agent_id` reaches a `PreToolUse` / `PostToolUse`
|
|
604
|
+
# payload inside a subagent is unverified on this host. That is Phase 0 Step 4,
|
|
605
|
+
# blocked on `raw-capture-needs-host-env` (the capture itself is already
|
|
606
|
+
# shipped — `dispatch_hook._maybe_capture_payload`). A neighbouring assertion
|
|
607
|
+
# from the same spike already came back negative: `agent_type` does NOT arrive
|
|
608
|
+
# on `SubagentStop` (18 of 25 stop records null), so the documented-field
|
|
609
|
+
# presence is not assumed for `agent_id` either.
|
|
610
|
+
# Until that spike lands the env path is the only one wired, and the
|
|
611
|
+
# pre_tool_use exemption above is unaffected either way — a payload-resolved
|
|
612
|
+
# `worker` still cannot drop a pre_tool_use concern.
|
|
593
613
|
roles:
|
|
594
614
|
worker:
|
|
595
615
|
# Orchestrator-only concerns (Phase 2.3): a worker never talks to the
|
|
@@ -73,8 +73,9 @@
|
|
|
73
73
|
* a `subagent_reaped` line is written for it. That line is not bookkeeping —
|
|
74
74
|
* it is the closest thing this instrument has to a direct observation of a
|
|
75
75
|
* never-returning dispatch, which is why the leak is reported rather than
|
|
76
|
-
* quietly swept. `session_id` rides on the record
|
|
77
|
-
* by session; nothing reads it yet and it is not
|
|
76
|
+
* quietly swept. `session_id` rides on the open record AND on both dispatch
|
|
77
|
+
* lines so a rate can be scoped by session; nothing reads it yet and it is not
|
|
78
|
+
* claimed to.
|
|
78
79
|
*
|
|
79
80
|
* ── Depth and concurrency (Step 3) ────────────────────────────────────────
|
|
80
81
|
*
|
|
@@ -598,6 +599,13 @@ function handleStart(root: string, payload: JsonObject, nowIso: string, sessionI
|
|
|
598
599
|
event: 'subagent_start',
|
|
599
600
|
ts: nowIso,
|
|
600
601
|
ref,
|
|
602
|
+
// Phase 1 Step 4 correction (b): without this the dispatch denominator
|
|
603
|
+
// is not per-session. Three sessions share one ledger file, so a rate
|
|
604
|
+
// computed over the file aggregates strangers — the measured window
|
|
605
|
+
// held 7 starts against 25 stops, one of them an agent type this
|
|
606
|
+
// session never dispatched. The open record already carried it; the
|
|
607
|
+
// appended line did not, and the line is what a rate is computed from.
|
|
608
|
+
session_id: sessionId,
|
|
601
609
|
agent_type: rec.agent_type,
|
|
602
610
|
parent_ref: parentRef,
|
|
603
611
|
depth,
|
|
@@ -606,7 +614,7 @@ function handleStart(root: string, payload: JsonObject, nowIso: string, sessionI
|
|
|
606
614
|
});
|
|
607
615
|
}
|
|
608
616
|
|
|
609
|
-
function handleStop(root: string, payload: JsonObject, nowIso: string): void {
|
|
617
|
+
function handleStop(root: string, payload: JsonObject, nowIso: string, sessionId: string | null): void {
|
|
610
618
|
const agentId = str(payload, 'agent_id', 'agentId');
|
|
611
619
|
if (agentId === null) {
|
|
612
620
|
appendUnidentified(root, 'subagent_stop', nowIso, payload);
|
|
@@ -631,6 +639,11 @@ function handleStop(root: string, payload: JsonObject, nowIso: string): void {
|
|
|
631
639
|
event: 'subagent_stop',
|
|
632
640
|
ts: nowIso,
|
|
633
641
|
ref,
|
|
642
|
+
// The session this stop was OBSERVED in, read from the same envelope
|
|
643
|
+
// position as the start's — never back-filled from `rec`, which would
|
|
644
|
+
// relabel a cross-session stop as belonging to the dispatching session
|
|
645
|
+
// and hide exactly the mismatch this field exists to expose.
|
|
646
|
+
session_id: sessionId,
|
|
634
647
|
agent_type: rec?.agent_type ?? str(payload, 'agent_type', 'agentType', 'subagent_type', 'subagentType'),
|
|
635
648
|
depth: rec?.depth ?? null,
|
|
636
649
|
depth_basis: rec?.depth_basis ?? null,
|
|
@@ -661,7 +674,7 @@ export function processEnvelope(envelope: JsonValue, consumerRoot: string): numb
|
|
|
661
674
|
reapStaleOpenRecords(consumerRoot, nowIso);
|
|
662
675
|
|
|
663
676
|
if (event === 'subagent_start') handleStart(consumerRoot, payload, nowIso, sessionId);
|
|
664
|
-
else handleStop(consumerRoot, payload, nowIso);
|
|
677
|
+
else handleStop(consumerRoot, payload, nowIso, sessionId);
|
|
665
678
|
} catch {
|
|
666
679
|
// Malformed payload, unreadable disk, anything — never disturb the run.
|
|
667
680
|
return EXIT_ALLOW;
|