@event4u/agent-config 9.13.0 → 9.15.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 +113 -67
- package/README.md +9 -1
- package/dist/agent-src/commands/agents/audit.md +7 -7
- package/dist/agent-src/commands/agents/optimize.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
- package/dist/agent-src/commands/chat-history.md +1 -0
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/council.md +1 -1
- package/dist/agent-src/commands/ghostwriter.md +1 -1
- package/dist/agent-src/commands/memory.md +2 -1
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +150 -0
- package/dist/agent-src/commands/optimize/skills.md +5 -5
- package/dist/agent-src/commands/optimize.md +12 -6
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/prediction-pool.md +4 -4
- package/dist/agent-src/commands/research/deep.md +1 -1
- package/dist/agent-src/commands/roadmap.md +2 -1
- package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
- package/dist/agent-src/commands/tdd.md +1 -0
- package/dist/agent-src/commands/tests.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +5 -5
- package/dist/agent-src/commands/video/from-script.md +1 -1
- package/dist/agent-src/commands/video/from-song.md +1 -1
- package/dist/agent-src/commands/video/scene.md +1 -1
- package/dist/agent-src/commands/video/stitch.md +1 -1
- package/dist/agent-src/commands/video/storyboard.md +1 -1
- package/dist/agent-src/commands/worktree/create.md +2 -1
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
- package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
- package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
- package/dist/agent-src/contexts/override-system.md +1 -1
- package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
- package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +2 -1
- package/dist/agent-src/rules/brand-consistency.md +1 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +1 -0
- package/dist/agent-src/rules/content-quoting-floor.md +1 -0
- package/dist/agent-src/rules/cross-source-consistency.md +0 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +0 -1
- package/dist/agent-src/rules/icon-consistency.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/invite-challenge.md +1 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/dist/agent-src/rules/media-governance-routing.md +4 -3
- package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
- package/dist/agent-src/rules/no-attribution-footers.md +1 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
- package/dist/agent-src/rules/onboarding-gate.md +1 -0
- package/dist/agent-src/rules/output-discipline.md +1 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
- package/dist/agent-src/rules/preservation-guard.md +1 -0
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
- package/dist/agent-src/rules/role-mode-adherence.md +1 -0
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
- package/dist/agent-src/rules/session-canary.md +9 -2
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/source-of-truth.md +1 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/token-budget-discipline.md +1 -0
- package/dist/agent-src/rules/token-efficiency.md +1 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
- package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
- package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
- package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
- package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/command.md +1 -1
- package/dist/agent-src/templates/copilot-instructions.md +3 -3
- package/dist/agent-src/templates/rule.md +2 -2
- package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/dist/agent-src/templates/skill-archive-note.md +5 -5
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/agent-config.js +66 -19
- package/dist/cli/agent-config.js.map +1 -1
- package/dist/cli/main.js +9 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/python/workspace_drive.js +2 -0
- package/dist/cli/python/workspace_drive.js.map +1 -1
- package/dist/cli/python/workspace_drive_health.js +2 -0
- package/dist/cli/python/workspace_drive_health.js.map +1 -1
- package/dist/cli/python/workspace_hosts.js +2 -0
- package/dist/cli/python/workspace_hosts.js.map +1 -1
- package/dist/cli/python/workspace_sessions.js +2 -0
- package/dist/cli/python/workspace_sessions.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
- package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
- package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-H574KOUH.js} +340 -462
- package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
- package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
- package/dist/cli-delegate/cmd_conformance.js +29 -23
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +163 -103
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +729 -430
- package/dist/install/install.mjs +700 -625
- package/dist/install/rule_scope.js +18 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +564 -424
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
- package/dist/scripts/_lib/scoped_projection.js +223 -0
- package/dist/scripts/_lib/scoped_projection.js.map +1 -0
- package/dist/scripts/_lib/surface_tiers.js +125 -0
- package/dist/scripts/_lib/surface_tiers.js.map +1 -0
- package/dist/scripts/_lib/token_count.js +166 -0
- package/dist/scripts/_lib/token_count.js.map +1 -0
- package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
- package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
- package/dist/scripts/condense.js +139 -9
- package/dist/scripts/condense.js.map +1 -1
- package/dist/scripts/project_thin_rules.js +450 -0
- package/dist/scripts/project_thin_rules.js.map +1 -0
- package/dist/server/routes/workspace.js +2 -0
- package/dist/server/routes/workspace.js.map +1 -1
- package/dist/server/schemas/settings.js +9 -3
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/index-DJY_vQVn.js +40 -0
- package/dist/ui/assets/index-DJY_vQVn.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/package.json +11 -11
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/src/config/agent-settings.template.yml +59 -11
- package/src/config/ci-local-parity.yml +8 -0
- package/src/config/discovery/unassigned-artefacts.yml +23 -25
- package/src/config/evaluator-budgets.json +35 -5
- package/src/config/gate-coverage.yml +147 -0
- package/src/config/gate-violation-baselines.json +25 -0
- package/src/install/rule_scope.ts +27 -2
- package/src/scripts/_cli/cmd_conformance.ts +13 -24
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
- package/src/scripts/_lib/exec_evidence.ts +5 -0
- package/src/scripts/_lib/gate_baseline.ts +213 -0
- package/src/scripts/_lib/scoped_projection.ts +262 -0
- package/src/scripts/_lib/tier_budget_routing.ts +234 -0
- package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
- package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
- package/src/scripts/_lib/trigger_routers.ts +17 -5
- package/src/scripts/adr/regenerate_index.ts +4 -3
- package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
- package/src/scripts/ai_council/compile_corpus.ts +1 -1
- package/src/scripts/audit_cloud_compatibility.ts +37 -2
- package/src/scripts/audit_initial_context.ts +53 -19
- package/src/scripts/audit_likelihood.ts +31 -2
- package/src/scripts/audit_user_type_axis.ts +58 -11
- package/src/scripts/bench_hook_latency.ts +172 -41
- package/src/scripts/build_mcp_catalog.ts +12 -1
- package/src/scripts/check_always_budget.ts +122 -11
- package/src/scripts/check_artefact_count_messaging.ts +128 -15
- package/src/scripts/check_augment_description_cap.ts +24 -3
- package/src/scripts/check_ci_strict_superset.ts +150 -0
- package/src/scripts/check_cluster_patterns.ts +100 -0
- package/src/scripts/check_consumer_scope_flip.ts +26 -4
- package/src/scripts/check_evaluator_budgets.ts +198 -19
- package/src/scripts/check_gate_coverage.ts +337 -1
- package/src/scripts/check_generator_output_coverage.ts +2 -1
- package/src/scripts/check_iron_law_prominence.ts +1 -1
- package/src/scripts/check_kernel_rule_bundle.ts +1 -1
- package/src/scripts/check_no_new_legacy_path.ts +167 -3
- package/src/scripts/check_pack_size.ts +18 -4
- package/src/scripts/check_portability.ts +37 -7
- package/src/scripts/check_references.ts +19 -3
- package/src/scripts/check_release_pr_shape.ts +13 -0
- package/src/scripts/check_token_regression.ts +9 -2
- package/src/scripts/condense.ts +155 -12
- package/src/scripts/cost/budget.mjs +63 -2
- package/src/scripts/count_scoped_projection.ts +104 -0
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/generate_capability_matrix.ts +8 -0
- package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
- package/src/scripts/hook_manifest.yaml +17 -3
- package/src/scripts/hooks/block_config_weakening.ts +337 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/probe_throttle.ts +75 -0
- package/src/scripts/install-hooks.sh +24 -2
- package/src/scripts/install.ts +15 -92
- package/src/scripts/lint_agents_md.ts +9 -3
- package/src/scripts/lint_archived_skills.ts +1 -1
- package/src/scripts/lint_artefact_frontmatter.ts +185 -37
- package/src/scripts/lint_command_routing.ts +21 -52
- package/src/scripts/lint_command_verbs.ts +47 -28
- package/src/scripts/lint_context_spine_usage.ts +38 -12
- package/src/scripts/lint_framework_leakage.ts +25 -6
- package/src/scripts/lint_handoffs.ts +49 -2
- package/src/scripts/lint_media_policy_linkage.ts +62 -30
- package/src/scripts/lint_namespace.ts +92 -7
- package/src/scripts/lint_new_skill_gate.ts +39 -31
- package/src/scripts/lint_originality_shingles.ts +7 -1
- package/src/scripts/lint_override_kernel_guard.ts +21 -3
- package/src/scripts/lint_pack_boundaries.ts +461 -91
- package/src/scripts/lint_pack_dependencies.ts +24 -57
- package/src/scripts/lint_persona_governance.ts +1 -1
- package/src/scripts/lint_role_experiences.ts +34 -5
- package/src/scripts/lint_trigger_precision.ts +142 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
- package/src/scripts/mcp_server/tools.ts +1 -1
- package/src/scripts/prepack-check.mjs +40 -1
- package/src/scripts/prepack_router_targets.d.mts +7 -0
- package/src/scripts/prepack_router_targets.mjs +93 -0
- package/src/scripts/print_required_checks.ts +72 -18
- package/src/scripts/profile_staleness_hook.ts +12 -0
- package/src/scripts/record_evaluator_measurements.mjs +105 -0
- package/src/scripts/recruit_preflight.sh +1 -1
- package/src/scripts/release.ts +105 -1
- package/src/scripts/router_target_paths.d.mts +4 -0
- package/src/scripts/router_target_paths.mjs +69 -0
- package/src/scripts/router_telemetry.ts +52 -1
- package/src/scripts/routing_doctor.ts +485 -0
- package/src/scripts/rule_trigger_eval.ts +403 -0
- package/src/scripts/schemas/rule.schema.json +5 -1
- package/src/scripts/session_canary_hook.ts +96 -5
- package/src/scripts/skill_linter.ts +90 -42
- package/src/scripts/smoke/skills.sh +1 -1
- package/src/scripts/sweep_dead_scan_roots.ts +356 -10
- package/src/scripts/trigger_coverage.ts +13 -2
- package/src/scripts/update_counts.ts +91 -12
- package/src/scripts/validate_telegraph_carveouts.ts +1 -1
- package/src/scripts/wrapper_freshness_hook.ts +14 -0
- package/src/server/schemas/settings.ts +19 -5
- package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
- package/dist/ui/assets/index-BpSQt5vm.js +0 -40
- package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import * as fs from 'node:fs';
|
|
33
33
|
import * as path from 'node:path';
|
|
34
34
|
|
|
35
|
+
import { resolve_rule_pack_scope } from '../scripts/_lib/scoped_projection.js';
|
|
35
36
|
import { rule_in_scope } from './ruleInScope.js';
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -67,19 +68,43 @@ function _list(value: unknown): readonly string[] | null {
|
|
|
67
68
|
* already-parsed settings object (same key contract as condense's
|
|
68
69
|
* `_read_projection_list` — absent / empty = legacy-all).
|
|
69
70
|
*/
|
|
70
|
-
export function ruleScopeFromSettings(
|
|
71
|
+
export function ruleScopeFromSettings(
|
|
72
|
+
settings: Record<string, unknown>,
|
|
73
|
+
packageRoot?: string,
|
|
74
|
+
): RuleScope {
|
|
71
75
|
const proj = settings['projection'];
|
|
72
76
|
if (typeof proj !== 'object' || proj === null || Array.isArray(proj)) {
|
|
73
77
|
return LEGACY_ALL;
|
|
74
78
|
}
|
|
75
79
|
const p = proj as Record<string, unknown>;
|
|
80
|
+
// `rule_packs: auto` derives the active-pack set — the SAME set the
|
|
81
|
+
// skill/command prune uses — so the rule axis cannot drift from the
|
|
82
|
+
// artefact axis. Resolving it needs the package root (packs.yml lives
|
|
83
|
+
// there); without one the sentinel degrades to the inactive axis rather
|
|
84
|
+
// than to an empty set, which would prune every pack-tagged rule.
|
|
85
|
+
const packs =
|
|
86
|
+
packageRoot === undefined
|
|
87
|
+
? _list(p['rule_packs'])
|
|
88
|
+
: resolve_rule_pack_scope(
|
|
89
|
+
p['rule_packs'],
|
|
90
|
+
packageRoot,
|
|
91
|
+
_list(_runtimeActivePacks(settings)) ?? [],
|
|
92
|
+
);
|
|
76
93
|
return {
|
|
77
94
|
workspaces: _list(p['rule_workspaces']),
|
|
78
|
-
packs
|
|
95
|
+
packs,
|
|
79
96
|
roles: _list(p['rule_roles']),
|
|
80
97
|
};
|
|
81
98
|
}
|
|
82
99
|
|
|
100
|
+
/** `runtime.active_packs` overlay from an already-parsed settings object. */
|
|
101
|
+
function _runtimeActivePacks(settings: Record<string, unknown>): unknown {
|
|
102
|
+
const rt = settings['runtime'];
|
|
103
|
+
return typeof rt === 'object' && rt !== null && !Array.isArray(rt)
|
|
104
|
+
? (rt as Record<string, unknown>)['active_packs']
|
|
105
|
+
: null;
|
|
106
|
+
}
|
|
107
|
+
|
|
83
108
|
/**
|
|
84
109
|
* Whether a rule FILE (path into the shipped `dist/agent-src/rules` tree)
|
|
85
110
|
* arrives in a consumer install under `scope`.
|
|
@@ -38,6 +38,7 @@ import { atomicAppendLine } from '../../install/atomic.js';
|
|
|
38
38
|
import { readRecentEntries } from '../../install/txlog.js';
|
|
39
39
|
import { detectToolPresence } from '../../install/detect.js';
|
|
40
40
|
import { resolvePackageRoot } from '../_lib/package_root.js';
|
|
41
|
+
import { routeTargetPathsPosix } from '../router_target_paths.mjs';
|
|
41
42
|
import {
|
|
42
43
|
_classify,
|
|
43
44
|
_collect_manifest_entries,
|
|
@@ -146,32 +147,20 @@ export function _check_txlog_clean(logPath: string = installLogPath()): Dict {
|
|
|
146
147
|
* Map one `routes_to` target (`<kind>:<id>`) to its candidate
|
|
147
148
|
* package-relative paths. The FIRST existing candidate wins.
|
|
148
149
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
150
|
+
* The kind→path table itself lives in `../router_target_paths.mjs` and is
|
|
151
|
+
* SHARED with `prepack-check.mjs` gate 4, which asserts every one of these
|
|
152
|
+
* targets lands inside the `files[]` whitelist at pack time. A second copy of
|
|
153
|
+
* the table would let the pack gate and this runtime resolver disagree about
|
|
154
|
+
* where a pointer lives — the packaging↔runtime gap Phase 4 of
|
|
155
|
+
* `road-to-gates-that-can-fail` closes. `contract:`'s two legitimate homes
|
|
156
|
+
* (public `docs/contracts/` pages and agent-facing
|
|
157
|
+
* `dist/agent-src/contexts/contracts/` contexts) are documented there.
|
|
153
158
|
*/
|
|
154
159
|
export function routeTargetPaths(target: string): string[] {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!id) return [];
|
|
160
|
-
switch (kind) {
|
|
161
|
-
case 'skill':
|
|
162
|
-
return [path.join('dist', 'agent-src', 'skills', id, 'SKILL.md')];
|
|
163
|
-
case 'command':
|
|
164
|
-
return [path.join('dist', 'agent-src', 'commands', `${id}.md`)];
|
|
165
|
-
case 'guideline':
|
|
166
|
-
return [path.join('docs', 'guidelines', `${id}.md`)];
|
|
167
|
-
case 'contract':
|
|
168
|
-
return [
|
|
169
|
-
path.join('docs', 'contracts', `${id}.md`),
|
|
170
|
-
path.join('dist', 'agent-src', 'contexts', 'contracts', `${id}.md`),
|
|
171
|
-
];
|
|
172
|
-
default:
|
|
173
|
-
return [];
|
|
174
|
-
}
|
|
160
|
+
// POSIX templates → platform paths. `path.join` over the split segments
|
|
161
|
+
// reproduces the previous per-kind `path.join(...)` calls exactly,
|
|
162
|
+
// including a slash-bearing id such as `guideline:a/b`.
|
|
163
|
+
return routeTargetPathsPosix(target).map((rel) => path.join(...rel.split('/')));
|
|
175
164
|
}
|
|
176
165
|
|
|
177
166
|
export function _check_router_pointers(packageRoot: string = PACKAGE_ROOT): Dict {
|
|
@@ -203,6 +203,11 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
|
|
|
203
203
|
last dispatcher feedback per concern, missing trampolines.
|
|
204
204
|
Wraps hooks:status. Read-only.
|
|
205
205
|
Flags: --format json|table, --strict (CI), --project-root <path>
|
|
206
|
+
routing:doctor Live routing diagnosis: per-gate ACTIVE/INACTIVE with the
|
|
207
|
+
concern's own reason (read-only probes), session_start chain,
|
|
208
|
+
router + projection freshness, host bridge status.
|
|
209
|
+
Flags: --platform <p>, --workspace <path>, --json, --strict,
|
|
210
|
+
--no-freshness
|
|
206
211
|
hooks:replay Replay a fixture through the universal dispatcher with
|
|
207
212
|
AGENT_CONFIG_REPLAY=1 (no writes under agents/runtime/state/).
|
|
208
213
|
Usage: hooks:replay --platform <name> --event <event>
|
|
@@ -755,6 +760,10 @@ cmd_hooks_doctor() {
|
|
|
755
760
|
exec_hook "src/scripts/hooks_doctor" "$@"
|
|
756
761
|
}
|
|
757
762
|
|
|
763
|
+
cmd_routing_doctor() {
|
|
764
|
+
exec_hook "src/scripts/routing_doctor" "$@"
|
|
765
|
+
}
|
|
766
|
+
|
|
758
767
|
cmd_hooks_replay() {
|
|
759
768
|
exec_hook "src/scripts/hooks/replay_hook" "$@"
|
|
760
769
|
}
|
|
@@ -1217,6 +1226,7 @@ main() {
|
|
|
1217
1226
|
dispatch:hook) cmd_dispatch_hook "$@" ;;
|
|
1218
1227
|
hooks:status) cmd_hooks_status "$@" ;;
|
|
1219
1228
|
hooks:doctor) cmd_hooks_doctor "$@" ;;
|
|
1229
|
+
routing:doctor) cmd_routing_doctor "$@" ;;
|
|
1220
1230
|
hooks:replay) cmd_hooks_replay "$@" ;;
|
|
1221
1231
|
reach:doctor) cmd_reach_doctor "$@" ;;
|
|
1222
1232
|
telemetry:record) cmd_telemetry_record "$@" ;;
|
|
@@ -108,19 +108,45 @@ export function build_claude_hook_matrix(manifest_path: string): ClaudeHookMatri
|
|
|
108
108
|
if (!concerns || (Array.isArray(concerns) && concerns.length === 0)) continue;
|
|
109
109
|
const native = ac_to_native[ac_event];
|
|
110
110
|
if (native === undefined) continue;
|
|
111
|
-
//
|
|
111
|
+
// Bundle fast path (road-to-hook-latency-repair Phase 2): invoke the
|
|
112
|
+
// precompiled dispatcher bundle directly — no bash shim, no CLI boot
|
|
113
|
+
// (measured ~450-500 ms CLI vs ~110 ms bundle per event on a 1-vCPU
|
|
114
|
+
// container; bench_hook_latency --via-cli pins it). Candidates:
|
|
115
|
+
// 1. project npm install (node_modules/@event4u/agent-config/…)
|
|
116
|
+
// 2. a source checkout — guarded by hook_manifest.yaml existing
|
|
117
|
+
// next to it, so an UNRELATED consumer file that happens to sit
|
|
118
|
+
// at dist/hooks/dispatch.js can never be executed as a hook
|
|
119
|
+
// (council 2026-08-03).
|
|
120
|
+
// `exec node` replaces the shell; stdin/exit codes flow through
|
|
121
|
+
// unchanged (a fail_closed concern still denies). Global installs
|
|
122
|
+
// have no project-relative bundle path and take the CLI fallback
|
|
123
|
+
// below, which boots the same bundle in-process via the launcher
|
|
124
|
+
// fast path (src/cli/agent-config.ts).
|
|
125
|
+
//
|
|
126
|
+
// Hook-resilience fallback (road-to-opt-subagent-harvest P1.4):
|
|
112
127
|
// local binary → PATH fallback → SILENT exit 0 when neither exists
|
|
113
128
|
// (a hook must never error-spam the loop on an uninstalled repo).
|
|
114
129
|
// The dispatcher's own exit code PROPAGATES when it runs — a
|
|
115
130
|
// fail_closed concern (e.g. block-no-verify) still denies; only the
|
|
116
131
|
// missing-binary case degrades open, which is the honest degrade
|
|
117
132
|
// (an absent dispatcher cannot evaluate anything).
|
|
133
|
+
// NOTE: the CLI fallback carries MANAGED_SIGNATURE — keep it, or
|
|
134
|
+
// previously installed managed entries orphan on refresh.
|
|
135
|
+
const dispatchArgs =
|
|
136
|
+
`--platform claude --event ${ac_event} --native-event ${native} ` +
|
|
137
|
+
`--project-dir "$CLAUDE_PROJECT_DIR" --min-version ${String(hook_spec)}`;
|
|
118
138
|
matrix[native] =
|
|
139
|
+
'B=""; ' +
|
|
140
|
+
'[ -f "$CLAUDE_PROJECT_DIR/node_modules/@event4u/agent-config/dist/hooks/dispatch.js" ] && ' +
|
|
141
|
+
'B="$CLAUDE_PROJECT_DIR/node_modules/@event4u/agent-config/dist/hooks/dispatch.js"; ' +
|
|
142
|
+
'[ -z "$B" ] && [ -f "$CLAUDE_PROJECT_DIR/dist/hooks/dispatch.js" ] && ' +
|
|
143
|
+
'[ -f "$CLAUDE_PROJECT_DIR/src/scripts/hook_manifest.yaml" ] && ' +
|
|
144
|
+
'B="$CLAUDE_PROJECT_DIR/dist/hooks/dispatch.js"; ' +
|
|
145
|
+
'if [ -n "$B" ] && command -v node >/dev/null 2>&1; then ' +
|
|
146
|
+
`exec node "$B" ${dispatchArgs}; fi; ` +
|
|
119
147
|
'BIN="$CLAUDE_PROJECT_DIR/agent-config"; [ -x "$BIN" ] || BIN=agent-config; ' +
|
|
120
148
|
'command -v "$BIN" >/dev/null 2>&1 || exit 0; ' +
|
|
121
|
-
`"$BIN" dispatch:hook
|
|
122
|
-
`--native-event ${native} --project-dir "$CLAUDE_PROJECT_DIR" ` +
|
|
123
|
-
`--min-version ${String(hook_spec)}`;
|
|
149
|
+
`"$BIN" dispatch:hook ${dispatchArgs}`;
|
|
124
150
|
}
|
|
125
151
|
return matrix;
|
|
126
152
|
}
|
|
@@ -47,6 +47,11 @@ export const ALLOWLIST_PREFIXES: readonly (readonly string[])[] = [
|
|
|
47
47
|
['skill_eval_coverage'],
|
|
48
48
|
['domain_soundness_status'],
|
|
49
49
|
['check_artefact_count_messaging'],
|
|
50
|
+
// `update_counts --check` re-derives every anchored number in the docs and
|
|
51
|
+
// exits non-zero on any disagreement — so its exit code IS the verdict for
|
|
52
|
+
// a published count, not merely evidence that some report exists. Read-only
|
|
53
|
+
// under `--check`; same deterministic, model-free class as its neighbours.
|
|
54
|
+
['update_counts'],
|
|
50
55
|
['check_enforcement_coverage'],
|
|
51
56
|
['check_token_regression'],
|
|
52
57
|
['lint_agent_security'],
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Violation ratchet for gates whose scan root was repaired.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS (council 2026-08-02, roadmap `road-to-gates-that-can-fail`
|
|
5
|
+
* blocker `dead-gate-finding-triage`): repairing a dead scan root does not
|
|
6
|
+
* create violations, it REVEALS violations that were invisible while the gate
|
|
7
|
+
* scanned nothing. Landing the repair with a hard fail would turn `main` red on
|
|
8
|
+
* debt the repairing change did not cause, blocking every unrelated merge
|
|
9
|
+
* behind unrelated copy-editing. Suppressing the findings would be the same
|
|
10
|
+
* green-on-nothing failure the repair exists to end.
|
|
11
|
+
*
|
|
12
|
+
* The adopted shape is a ratchet:
|
|
13
|
+
*
|
|
14
|
+
* The repair records the revealed count as a per-gate baseline in
|
|
15
|
+
* `src/config/gate-violation-baselines.json`. The gate fails only when its
|
|
16
|
+
* count rises ABOVE the baseline. Lowering a baseline is a normal commit;
|
|
17
|
+
* raising one is a defect.
|
|
18
|
+
*
|
|
19
|
+
* This is not an allowlist: no individual violation is named or excused, the
|
|
20
|
+
* number is committed, and every change to it shows as a delta in the diff.
|
|
21
|
+
*
|
|
22
|
+
* ## The anti-fossilization clause — why entries expire
|
|
23
|
+
*
|
|
24
|
+
* The council's own strongest objection to the ratchet was that a baseline
|
|
25
|
+
* which never moves hardens from "debt" into "configuration", at which point it
|
|
26
|
+
* IS suppression with extra steps. So an entry is not permanent: it carries the
|
|
27
|
+
* date its repair landed, and after {@link STALE_AFTER_DAYS} days a baseline
|
|
28
|
+
* that has not been lowered fails the gate with an escalation message. Clearing
|
|
29
|
+
* it means one of three visible acts — fix the findings, lower the count, or
|
|
30
|
+
* write a `reaffirmed` block whose non-empty reason lands in the diff.
|
|
31
|
+
*
|
|
32
|
+
* Dates are plain `YYYY-MM-DD`; ages are whole days in UTC, so the verdict does
|
|
33
|
+
* not depend on the machine's timezone.
|
|
34
|
+
*/
|
|
35
|
+
import * as fs from 'node:fs';
|
|
36
|
+
import * as path from 'node:path';
|
|
37
|
+
|
|
38
|
+
/** Days a baseline may sit unchanged before it must be fixed or reaffirmed. */
|
|
39
|
+
export const STALE_AFTER_DAYS = 56;
|
|
40
|
+
|
|
41
|
+
/** Repo-relative location of the committed baseline file. */
|
|
42
|
+
export const BASELINE_REL = 'src/config/gate-violation-baselines.json';
|
|
43
|
+
|
|
44
|
+
/** A single gate's recorded debt. */
|
|
45
|
+
export interface BaselineEntry {
|
|
46
|
+
/** Violation count revealed when the scan root was repaired. */
|
|
47
|
+
count: number;
|
|
48
|
+
/** `YYYY-MM-DD` the repair (or the last lowering) landed. */
|
|
49
|
+
landed: string;
|
|
50
|
+
/** What the counted violations are, so a reader need not re-derive them. */
|
|
51
|
+
note: string;
|
|
52
|
+
/** Optional clock reset — requires a reason, and the reason lands in the diff. */
|
|
53
|
+
reaffirmed?: { date: string; reason: string };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BaselineFile {
|
|
57
|
+
/** Human-facing pointer back to the rule that governs this file. */
|
|
58
|
+
$comment?: string;
|
|
59
|
+
gates: Record<string, BaselineEntry>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type BaselineStatus =
|
|
63
|
+
/** Count is at or under the baseline and the entry is still fresh. */
|
|
64
|
+
| 'within'
|
|
65
|
+
/** Count is below the baseline — the ratchet should be tightened. */
|
|
66
|
+
| 'improved'
|
|
67
|
+
/** Count rose above the baseline. This is the defect the ratchet catches. */
|
|
68
|
+
| 'regressed'
|
|
69
|
+
/** Count is within, but the entry has sat unchanged past the expiry. */
|
|
70
|
+
| 'stale'
|
|
71
|
+
/** No baseline recorded — zero violations is the only passing state. */
|
|
72
|
+
| 'unbaselined';
|
|
73
|
+
|
|
74
|
+
export interface BaselineVerdict {
|
|
75
|
+
status: BaselineStatus;
|
|
76
|
+
/** True when the gate may exit 0. */
|
|
77
|
+
ok: boolean;
|
|
78
|
+
/** The effective baseline (0 when no entry exists). */
|
|
79
|
+
baseline: number;
|
|
80
|
+
actual: number;
|
|
81
|
+
/** Whole days since `landed` (or since `reaffirmed.date` when present). */
|
|
82
|
+
ageDays: number | null;
|
|
83
|
+
/** One line, ready for stdout (ok) or stderr (not ok). */
|
|
84
|
+
message: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseIsoDate(value: string): number | null {
|
|
88
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const ms = Date.parse(`${value}T00:00:00Z`);
|
|
92
|
+
return Number.isNaN(ms) ? null : ms;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function wholeDaysBetween(fromMs: number, toMs: number): number {
|
|
96
|
+
return Math.floor((toMs - fromMs) / 86_400_000);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Read the committed baseline file. A missing file is an empty ratchet, not an error. */
|
|
100
|
+
export function loadBaselines(repoRoot: string, relPath: string = BASELINE_REL): BaselineFile {
|
|
101
|
+
const abs = path.join(repoRoot, relPath);
|
|
102
|
+
let raw: string;
|
|
103
|
+
try {
|
|
104
|
+
raw = fs.readFileSync(abs, 'utf8');
|
|
105
|
+
} catch {
|
|
106
|
+
return { gates: {} };
|
|
107
|
+
}
|
|
108
|
+
const parsed = JSON.parse(raw) as Partial<BaselineFile>;
|
|
109
|
+
const out: BaselineFile = { gates: parsed.gates ?? {} };
|
|
110
|
+
if (parsed.$comment !== undefined) out.$comment = parsed.$comment;
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface RatchetOptions {
|
|
115
|
+
/** Gate name — the key in the baseline file. */
|
|
116
|
+
gate: string;
|
|
117
|
+
/** How many violations this run found. */
|
|
118
|
+
actual: number;
|
|
119
|
+
/** Absolute repo root. */
|
|
120
|
+
repoRoot: string;
|
|
121
|
+
/** Override the baseline file location (tests). */
|
|
122
|
+
baselineRel?: string;
|
|
123
|
+
/** Override "now" as `YYYY-MM-DD` (tests); defaults to the current UTC date. */
|
|
124
|
+
today?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Judge a violation count against its recorded baseline.
|
|
129
|
+
*
|
|
130
|
+
* Callers keep their own reporting and exit codes — this returns a verdict, it
|
|
131
|
+
* does not print or exit. A gate with no entry behaves exactly as it did before
|
|
132
|
+
* the ratchet existed: any violation fails.
|
|
133
|
+
*/
|
|
134
|
+
export function checkRatchet(opts: RatchetOptions): BaselineVerdict {
|
|
135
|
+
const { gate, actual } = opts;
|
|
136
|
+
const file = loadBaselines(opts.repoRoot, opts.baselineRel ?? BASELINE_REL);
|
|
137
|
+
const entry = file.gates[gate];
|
|
138
|
+
|
|
139
|
+
if (entry === undefined) {
|
|
140
|
+
return {
|
|
141
|
+
status: 'unbaselined',
|
|
142
|
+
ok: actual === 0,
|
|
143
|
+
baseline: 0,
|
|
144
|
+
actual,
|
|
145
|
+
ageDays: null,
|
|
146
|
+
message:
|
|
147
|
+
actual === 0
|
|
148
|
+
? `${gate}: 0 violation(s), no baseline needed.`
|
|
149
|
+
: `${gate}: ${actual} violation(s) and no recorded baseline — fix them, or ` +
|
|
150
|
+
`record the count in ${BASELINE_REL} if this is a scan-root repair ` +
|
|
151
|
+
'revealing pre-existing debt.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const anchor = entry.reaffirmed?.date ?? entry.landed;
|
|
156
|
+
const anchorMs = parseIsoDate(anchor);
|
|
157
|
+
const todayMs = parseIsoDate(opts.today ?? new Date().toISOString().slice(0, 10));
|
|
158
|
+
const ageDays = anchorMs !== null && todayMs !== null ? wholeDaysBetween(anchorMs, todayMs) : null;
|
|
159
|
+
|
|
160
|
+
if (actual > entry.count) {
|
|
161
|
+
return {
|
|
162
|
+
status: 'regressed',
|
|
163
|
+
ok: false,
|
|
164
|
+
baseline: entry.count,
|
|
165
|
+
actual,
|
|
166
|
+
ageDays,
|
|
167
|
+
message:
|
|
168
|
+
`${gate}: ${actual} violation(s) against a baseline of ${entry.count} — ` +
|
|
169
|
+
`${actual - entry.count} new. A ratchet only turns one way: fix the new ` +
|
|
170
|
+
`violation(s). Raising the baseline in ${BASELINE_REL} is a defect, not a fix.`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (actual < entry.count) {
|
|
175
|
+
return {
|
|
176
|
+
status: 'improved',
|
|
177
|
+
ok: true,
|
|
178
|
+
baseline: entry.count,
|
|
179
|
+
actual,
|
|
180
|
+
ageDays,
|
|
181
|
+
message:
|
|
182
|
+
`${gate}: ${actual} violation(s), baseline ${entry.count} — ratchet is loose. ` +
|
|
183
|
+
`Lower it to ${actual} in ${BASELINE_REL} (and refresh \`landed\`) so the ` +
|
|
184
|
+
'gain cannot be silently given back.',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (ageDays !== null && ageDays > STALE_AFTER_DAYS) {
|
|
189
|
+
return {
|
|
190
|
+
status: 'stale',
|
|
191
|
+
ok: false,
|
|
192
|
+
baseline: entry.count,
|
|
193
|
+
actual,
|
|
194
|
+
ageDays,
|
|
195
|
+
message:
|
|
196
|
+
`${gate}: baseline of ${entry.count} has not moved in ${ageDays} days ` +
|
|
197
|
+
`(limit ${STALE_AFTER_DAYS}). A baseline that never drops is suppression with ` +
|
|
198
|
+
'extra steps. Fix the findings, lower the count, or add a `reaffirmed` block ' +
|
|
199
|
+
`with a reason in ${BASELINE_REL}. Debt: ${entry.note}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
status: 'within',
|
|
205
|
+
ok: true,
|
|
206
|
+
baseline: entry.count,
|
|
207
|
+
actual,
|
|
208
|
+
ageDays,
|
|
209
|
+
message:
|
|
210
|
+
`${gate}: ${actual} violation(s) at baseline${ageDays === null ? '' : `, age ${ageDays}d`} ` +
|
|
211
|
+
`— known debt, not new breakage. ${entry.note}`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scoped-projection predicate — ONE implementation, two callers.
|
|
3
|
+
*
|
|
4
|
+
* `projection.mode: scoped` narrows a GLOBAL deploy to the active profile's
|
|
5
|
+
* packs. Two places need to know exactly which skills survive that prune:
|
|
6
|
+
*
|
|
7
|
+
* 1. `install.ts` — which artefacts it actually writes to the user's tree.
|
|
8
|
+
* 2. `count_scoped_projection.ts` — the number the claims ledger publishes.
|
|
9
|
+
*
|
|
10
|
+
* Before this module those were the same rule written once and *typed* a
|
|
11
|
+
* second time into `docs/CLAIMS.md`. The published pair drifted from the
|
|
12
|
+
* benchmark doc it cited as its own method (217/288 vs 215/286) and grew by
|
|
13
|
+
* one on every skill added, because nothing could compare them. The fix is
|
|
14
|
+
* not a second counter — a second counting path that disagrees with the
|
|
15
|
+
* first is the failure this replaces. It is this module: the installer and
|
|
16
|
+
* the counter call the same functions, so a divergence is impossible rather
|
|
17
|
+
* than merely unlikely.
|
|
18
|
+
*
|
|
19
|
+
* Consumers: `install.ts` (the prune itself) and `count_scoped_projection.ts`
|
|
20
|
+
* (the published number).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import type * as YamlModule from 'yaml';
|
|
27
|
+
|
|
28
|
+
import * as surface_tiers from './surface_tiers.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ESM-safe `require`. A bare `require('yaml')` is `undefined` under tsx (the
|
|
32
|
+
* runner every `./scripts-run` gate uses), so the older lazy-require pattern
|
|
33
|
+
* elsewhere in this tree degrades to "no yaml" silently. `createRequire`
|
|
34
|
+
* keeps the load lazy — `yaml` stays out of the installer bundle's static
|
|
35
|
+
* graph — while actually resolving.
|
|
36
|
+
*/
|
|
37
|
+
const _require = createRequire(import.meta.url);
|
|
38
|
+
|
|
39
|
+
/** `workspaces` values whose packs are ALWAYS active under `scoped` mode. */
|
|
40
|
+
export const SCOPED_ACTIVE_WORKSPACES: ReadonlySet<string> = new Set([
|
|
41
|
+
'engineering',
|
|
42
|
+
'agent-config-maintainer',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export interface PackRecord {
|
|
46
|
+
id: string;
|
|
47
|
+
workspaces: string[];
|
|
48
|
+
requires: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
52
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Lazy YAML safe_load mirroring PyYAML (version 1.1); `null` when absent. */
|
|
56
|
+
function yamlSafeLoad(text: string): unknown {
|
|
57
|
+
let YAML: typeof YamlModule;
|
|
58
|
+
try {
|
|
59
|
+
YAML = _require('yaml') as typeof YamlModule;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return YAML.parse(text, { version: '1.1' }) ?? null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Repo-relative location of the pack vocabulary. */
|
|
71
|
+
export const PACKS_YML_REL = path.join('src', 'config', 'discovery', 'packs.yml');
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse `src/config/discovery/packs.yml` into `{id, workspaces, requires}`
|
|
75
|
+
* records. `requires` falls back to the legacy `requires_hint` field name
|
|
76
|
+
* when `requires` is absent.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately THROWS on a missing file or a parse result that is not a
|
|
79
|
+
* list — an empty active-pack set would be the LEAST safe fallback here
|
|
80
|
+
* (it would prune every tagged artefact, the opposite of the fail-safe
|
|
81
|
+
* contract). Callers wrap this in a try/catch that restores the full tree
|
|
82
|
+
* on any error instead.
|
|
83
|
+
*/
|
|
84
|
+
export function load_packs_registry(package_root: string): PackRecord[] {
|
|
85
|
+
const vocab_path = path.join(package_root, PACKS_YML_REL);
|
|
86
|
+
const data = yamlSafeLoad(fs.readFileSync(vocab_path, 'utf-8'));
|
|
87
|
+
if (!Array.isArray(data)) {
|
|
88
|
+
throw new Error(`packs.yml did not parse to a list: ${vocab_path}`);
|
|
89
|
+
}
|
|
90
|
+
const out: PackRecord[] = [];
|
|
91
|
+
for (const entry of data) {
|
|
92
|
+
if (!isPlainObject(entry)) continue;
|
|
93
|
+
const id = entry['id'];
|
|
94
|
+
if (typeof id !== 'string' || id === '') continue;
|
|
95
|
+
const workspaces_raw = entry['workspaces'];
|
|
96
|
+
const workspaces = Array.isArray(workspaces_raw)
|
|
97
|
+
? workspaces_raw.filter((w): w is string => typeof w === 'string')
|
|
98
|
+
: [];
|
|
99
|
+
const requires_raw = entry['requires'] ?? entry['requires_hint'];
|
|
100
|
+
const requires = Array.isArray(requires_raw)
|
|
101
|
+
? requires_raw.filter((r): r is string => typeof r === 'string')
|
|
102
|
+
: [];
|
|
103
|
+
out.push({ id, workspaces, requires });
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Active pack id set for `scoped` mode: every pack whose `workspaces`
|
|
110
|
+
* intersects `SCOPED_ACTIVE_WORKSPACES`, unioned with `runtime_active_packs`,
|
|
111
|
+
* expanded over the `requires` graph (transitive closure).
|
|
112
|
+
*/
|
|
113
|
+
export function compute_active_pack_ids(
|
|
114
|
+
packs: readonly PackRecord[],
|
|
115
|
+
runtime_active_packs: readonly string[],
|
|
116
|
+
): Set<string> {
|
|
117
|
+
const by_id = new Map<string, PackRecord>();
|
|
118
|
+
for (const p of packs) by_id.set(p.id, p);
|
|
119
|
+
|
|
120
|
+
const active = new Set<string>();
|
|
121
|
+
for (const p of packs) {
|
|
122
|
+
if (p.workspaces.some((w) => SCOPED_ACTIVE_WORKSPACES.has(w))) {
|
|
123
|
+
active.add(p.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const id of runtime_active_packs) {
|
|
127
|
+
active.add(id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let frontier = [...active];
|
|
131
|
+
while (frontier.length > 0) {
|
|
132
|
+
const next: string[] = [];
|
|
133
|
+
for (const id of frontier) {
|
|
134
|
+
const rec = by_id.get(id);
|
|
135
|
+
if (rec === undefined) continue;
|
|
136
|
+
for (const dep of rec.requires) {
|
|
137
|
+
if (!active.has(dep)) {
|
|
138
|
+
active.add(dep);
|
|
139
|
+
next.push(dep);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
frontier = next;
|
|
144
|
+
}
|
|
145
|
+
return active;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The prune predicate itself: `true` when this artefact is DROPPED under
|
|
150
|
+
* `scoped`. Untagged artefacts (empty `packs:` frontmatter) are core/shared
|
|
151
|
+
* and always kept; a tagged artefact survives iff its tags intersect the
|
|
152
|
+
* active set.
|
|
153
|
+
*/
|
|
154
|
+
export function is_pruned_under_scoped(md_path: string, active_ids: ReadonlySet<string>): boolean {
|
|
155
|
+
const packs = surface_tiers.frontmatter_packs(md_path);
|
|
156
|
+
if (packs.size === 0) return false;
|
|
157
|
+
for (const id of packs) {
|
|
158
|
+
if (active_ids.has(id)) return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sentinel value for `projection.rule_packs`: derive the pack scope from the
|
|
165
|
+
* active-pack set instead of listing ids by hand.
|
|
166
|
+
*
|
|
167
|
+
* The rule layer's pack axis was wired end-to-end but shipped inactive
|
|
168
|
+
* (`rule_packs: []`), so a rule whose own body says "auto-activates when
|
|
169
|
+
* pack-X is installed" projected into installs that do not have pack-X,
|
|
170
|
+
* where it guards nothing. Listing the active ids by hand is not a fix — the
|
|
171
|
+
* list would need re-typing whenever a pack is added, which is the
|
|
172
|
+
* two-counting-paths failure this module exists to prevent. `auto` reuses
|
|
173
|
+
* `compute_active_pack_ids`, the SAME set the skill/command prune uses, so
|
|
174
|
+
* the rule axis cannot drift from the artefact axis.
|
|
175
|
+
*/
|
|
176
|
+
export const RULE_PACKS_AUTO = 'auto';
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve a raw `projection.rule_packs` setting into the pack scope
|
|
180
|
+
* `rule_in_scope` consumes.
|
|
181
|
+
*
|
|
182
|
+
* - `'auto'` (scalar or single-element list) → the derived active-pack set.
|
|
183
|
+
* - a non-empty list → that list, verbatim.
|
|
184
|
+
* - anything else (absent, `[]`, wrong type) → `null`, axis inactive.
|
|
185
|
+
*
|
|
186
|
+
* A derivation failure (unreadable / malformed `packs.yml`) returns `null`,
|
|
187
|
+
* not an empty set: an empty set would prune every pack-tagged rule, the
|
|
188
|
+
* opposite of the fail-safe contract `rule_in_scope` documents.
|
|
189
|
+
*/
|
|
190
|
+
export function resolve_rule_pack_scope(
|
|
191
|
+
raw: unknown,
|
|
192
|
+
package_root: string,
|
|
193
|
+
runtime_active_packs: readonly string[] = [],
|
|
194
|
+
): string[] | null {
|
|
195
|
+
const is_auto =
|
|
196
|
+
raw === RULE_PACKS_AUTO ||
|
|
197
|
+
(Array.isArray(raw) && raw.length === 1 && raw[0] === RULE_PACKS_AUTO);
|
|
198
|
+
if (is_auto) {
|
|
199
|
+
try {
|
|
200
|
+
return [
|
|
201
|
+
...compute_active_pack_ids(load_packs_registry(package_root), runtime_active_packs),
|
|
202
|
+
].sort();
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
208
|
+
return raw.map((v) => String(v));
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface ScopedProjectionStats {
|
|
214
|
+
/** Skills a default `projection.mode: scoped` install deploys. */
|
|
215
|
+
projected: number;
|
|
216
|
+
/** Every skill in the catalog — identical to canonical `count('skills')`. */
|
|
217
|
+
total: number;
|
|
218
|
+
/** `total - projected`; the reduction the claim reports. */
|
|
219
|
+
pruned: number;
|
|
220
|
+
/** Pack ids active by default, sorted — the requires-closure result. */
|
|
221
|
+
active_packs: string[];
|
|
222
|
+
/** The workspaces whose packs are always on under `scoped`. */
|
|
223
|
+
workspaces: string[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Partition a skill catalog with the installer's own predicate.
|
|
228
|
+
*
|
|
229
|
+
* The caller supplies the file set — in practice always
|
|
230
|
+
* `update_counts.iter_skills()`, the walk behind the canonical
|
|
231
|
+
* `count('skills')`, so `projected + pruned === total` holds by
|
|
232
|
+
* construction. Taking it as a parameter (rather than importing
|
|
233
|
+
* `update_counts` here) keeps this module free of a cycle: `update_counts`
|
|
234
|
+
* imports it, not the other way round.
|
|
235
|
+
*
|
|
236
|
+
* `runtime_active_packs` defaults to empty: the published claim is about the
|
|
237
|
+
* DEFAULT for a new install, and a fresh machine has no overlay.
|
|
238
|
+
*/
|
|
239
|
+
export function scoped_projection_stats(
|
|
240
|
+
package_root: string,
|
|
241
|
+
skills: Iterable<string>,
|
|
242
|
+
runtime_active_packs: readonly string[] = [],
|
|
243
|
+
): ScopedProjectionStats {
|
|
244
|
+
const active = compute_active_pack_ids(load_packs_registry(package_root), runtime_active_packs);
|
|
245
|
+
|
|
246
|
+
let projected = 0;
|
|
247
|
+
let total = 0;
|
|
248
|
+
for (const skill of skills) {
|
|
249
|
+
total += 1;
|
|
250
|
+
if (!is_pruned_under_scoped(skill, active)) {
|
|
251
|
+
projected += 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
projected,
|
|
257
|
+
total,
|
|
258
|
+
pruned: total - projected,
|
|
259
|
+
active_packs: [...active].sort(),
|
|
260
|
+
workspaces: [...SCOPED_ACTIVE_WORKSPACES].sort(),
|
|
261
|
+
};
|
|
262
|
+
}
|