@event4u/agent-config 9.14.0 → 9.16.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 +118 -99
- package/README.md +8 -0
- 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 +5 -2
- package/dist/agent-src/commands/optimize/skills.md +5 -5
- 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/mcp/content.js +1 -1
- package/dist/cli/mcp/content.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-GCVFLBP2.js → chunk-H574KOUH.js} +338 -494
- 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 +127 -95
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +729 -430
- package/dist/install/install.mjs +52 -11
- package/dist/install/rule_scope.js +18 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +518 -411
- 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 +3 -1
- 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/shared/userMd/utils.js +1 -1
- package/dist/shared/userMd/utils.js.map +1 -1
- package/dist/ui/assets/index-DmR0mbk4.js +26 -0
- package/dist/ui/assets/index-DmR0mbk4.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 +14 -14
- 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 +61 -11
- package/src/config/budget-routing.json +7 -0
- 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/gate_baseline.ts +213 -0
- package/src/scripts/_lib/scoped_projection.ts +50 -0
- package/src/scripts/_lib/tier_budget_routing.ts +360 -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/anchor_scoring_run.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/bench_persona_placebo.ts +1 -1
- package/src/scripts/bench_quality_run.ts +1 -1
- package/src/scripts/build_mcp_catalog.ts +12 -1
- package/src/scripts/check_always_budget.ts +122 -11
- package/src/scripts/check_augment_description_cap.ts +24 -3
- package/src/scripts/check_ci_strict_superset.ts +150 -0
- package/src/scripts/check_cli_registry_budget_sync.ts +168 -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 +338 -2
- 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_quality_golden.ts +1 -1
- package/src/scripts/check_token_regression.ts +9 -2
- package/src/scripts/condense.ts +155 -12
- package/src/scripts/cost/budget.mjs +77 -2
- 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 +2 -2
- 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 +214 -7
- 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 +560 -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 +9 -28
- 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 +1 -1
- 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/src/shared/userMd/utils.ts +1 -1
- 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
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* rule_trigger_eval — RULES mode for the live LLM trigger-eval harness
|
|
4
|
+
* (road-to-tested-routing Phase 4, advisory only, never gating).
|
|
5
|
+
*
|
|
6
|
+
* Sibling of `skill_trigger_eval.ts` rather than a `--mode` flag on it: the
|
|
7
|
+
* skill CLI is a byte-for-byte port of the retired Python implementation
|
|
8
|
+
* whose contract ("mirror EXACTLY. No behaviour changes") is pinned by its
|
|
9
|
+
* own test suite, and its /dev/tty confirmation gate must stay untouched.
|
|
10
|
+
* This script REUSES its abstractions (SkillMeta shape, MockRouter,
|
|
11
|
+
* compute_metrics, _extract_field, the fetch routers) and adds only what is
|
|
12
|
+
* rules-specific:
|
|
13
|
+
*
|
|
14
|
+
* - catalog: rule id + frontmatter description from
|
|
15
|
+
* `dist/agent-src/rules/<id>.md`, ids from `dist/router.json`
|
|
16
|
+
* tier_1 + tier_2 (kernel excluded — kernel rules always load, there is
|
|
17
|
+
* nothing to route);
|
|
18
|
+
* - suite: derived from `tests/eval/routing-matrix/*.yaml` — each positive
|
|
19
|
+
* becomes a case expecting its rule to be consulted, each near-miss a
|
|
20
|
+
* case expecting it NOT to be; capped deterministically (FNV-1a hash
|
|
21
|
+
* order, no Math.random) so a live run stays affordable;
|
|
22
|
+
* - question: "which rules would you consult for this prompt", answered as
|
|
23
|
+
* the same `{"would_load": [...]}` JSON the routers already parse.
|
|
24
|
+
*
|
|
25
|
+
* Boundaries (mirroring `trigger_eval_rotation.ts`):
|
|
26
|
+
* - The local interactive `skill_trigger_eval` CLI and its /dev/tty
|
|
27
|
+
* confirmation gate are untouched — this script is the CI-only live path;
|
|
28
|
+
* live authorization derives exclusively from the key file the canary
|
|
29
|
+
* workflow materializes from repo secrets. No env-var key fallback.
|
|
30
|
+
* - `--dry-run` (MockRouter, no key, no spend) exercises the plumbing only;
|
|
31
|
+
* the advisory floor is REPORTED but never fails the run.
|
|
32
|
+
* - A live floor breach fails the SCHEDULED canary job only — PRs are never
|
|
33
|
+
* blocked by live results.
|
|
34
|
+
*
|
|
35
|
+
* Exit codes: 0 pass (always, in --dry-run) · 1 live floor breach ·
|
|
36
|
+
* 2 usage / IO error.
|
|
37
|
+
*/
|
|
38
|
+
import * as fs from 'node:fs';
|
|
39
|
+
import * as path from 'node:path';
|
|
40
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
+
|
|
42
|
+
import { parse as parseYaml } from 'yaml';
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
MockRouter,
|
|
46
|
+
_extract_field,
|
|
47
|
+
compute_metrics,
|
|
48
|
+
type Metrics,
|
|
49
|
+
type SkillMeta,
|
|
50
|
+
type TriggerRouter,
|
|
51
|
+
} from './skill_trigger_eval.js';
|
|
52
|
+
import { AnthropicFetchRouter, loadKeyFromFile } from './_lib/trigger_routers.js';
|
|
53
|
+
import { RULES_MODE_FLOOR } from './_lib/trigger_eval_floors.js';
|
|
54
|
+
|
|
55
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
57
|
+
const ROUTER_JSON = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
58
|
+
const RULES_DIR = path.join(REPO_ROOT, 'dist', 'agent-src', 'rules');
|
|
59
|
+
const MATRIX_DIR = path.join(REPO_ROOT, 'tests', 'eval', 'routing-matrix');
|
|
60
|
+
const DEFAULT_OUT = path.join(
|
|
61
|
+
REPO_ROOT,
|
|
62
|
+
'internal',
|
|
63
|
+
'evals',
|
|
64
|
+
'results',
|
|
65
|
+
'rotation',
|
|
66
|
+
'rules-mode.json',
|
|
67
|
+
);
|
|
68
|
+
export const DEFAULT_MAX_CASES = 40;
|
|
69
|
+
export const DEFAULT_RULES_MODEL = 'claude-haiku-4-5-20251001';
|
|
70
|
+
|
|
71
|
+
/** A rule catalogue entry is structurally a SkillMeta (name = rule id). */
|
|
72
|
+
export type RuleMeta = SkillMeta;
|
|
73
|
+
|
|
74
|
+
/** Router that may expose an async route (fetch-based live routers). */
|
|
75
|
+
type AsyncCapableRouter = TriggerRouter & {
|
|
76
|
+
routeAsync?: (query: string, skills: SkillMeta[]) => Promise<[string[], number, number]>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const RULES_PROMPT_HEADER = `You are a rule-routing oracle. Given the rule catalogue below
|
|
80
|
+
and a single user prompt, return ONLY the JSON object {"would_load": [...]}
|
|
81
|
+
listing the rule ids you would consult before acting on the prompt.
|
|
82
|
+
|
|
83
|
+
Rules:
|
|
84
|
+
- Use the rule description verbatim as the only routing signal.
|
|
85
|
+
- Return at most 6 rule ids.
|
|
86
|
+
- If no rule applies, return {"would_load": []}.
|
|
87
|
+
- Output ONLY the JSON. No prose, no code fences.
|
|
88
|
+
|
|
89
|
+
Rule catalogue (id :: description):
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
// ── Catalog ──────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build the rule catalogue: tier_1 + tier_2 ids from `dist/router.json`
|
|
96
|
+
* (kernel excluded — kernel always loads), description from the rule file's
|
|
97
|
+
* frontmatter. A rule whose file or description is missing is a build error:
|
|
98
|
+
* the projection guarantees both, so a gap means the inputs are stale.
|
|
99
|
+
*/
|
|
100
|
+
export function build_rule_catalog(
|
|
101
|
+
routerJsonPath: string = ROUTER_JSON,
|
|
102
|
+
rulesDir: string = RULES_DIR,
|
|
103
|
+
): RuleMeta[] {
|
|
104
|
+
const router = JSON.parse(fs.readFileSync(routerJsonPath, 'utf-8')) as {
|
|
105
|
+
tier_1?: Array<{ id?: unknown }>;
|
|
106
|
+
tier_2?: Array<{ id?: unknown }>;
|
|
107
|
+
};
|
|
108
|
+
const ids: string[] = [];
|
|
109
|
+
for (const tier of [router.tier_1, router.tier_2]) {
|
|
110
|
+
if (!Array.isArray(tier)) continue;
|
|
111
|
+
for (const entry of tier) {
|
|
112
|
+
if (typeof entry?.id === 'string' && entry.id) ids.push(entry.id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
ids.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
116
|
+
const catalog: RuleMeta[] = [];
|
|
117
|
+
for (const id of ids) {
|
|
118
|
+
const rulePath = path.join(rulesDir, `${id}.md`);
|
|
119
|
+
const text = fs.readFileSync(rulePath, 'utf-8');
|
|
120
|
+
if (!text.startsWith('---')) {
|
|
121
|
+
throw new Error(`${rulePath}: no frontmatter`);
|
|
122
|
+
}
|
|
123
|
+
const end = text.indexOf('\n---', 3);
|
|
124
|
+
if (end < 0) {
|
|
125
|
+
throw new Error(`${rulePath}: unterminated frontmatter`);
|
|
126
|
+
}
|
|
127
|
+
const description = _extract_field(text.slice(3, end), 'description');
|
|
128
|
+
if (description === null || description === '') {
|
|
129
|
+
throw new Error(`${rulePath}: frontmatter has no description`);
|
|
130
|
+
}
|
|
131
|
+
catalog.push({ name: id, description });
|
|
132
|
+
}
|
|
133
|
+
return catalog;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Derived suite ────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
export interface RuleCase {
|
|
139
|
+
/** Rule id under test for this case. */
|
|
140
|
+
rule: string;
|
|
141
|
+
/** The user prompt sent to the router. */
|
|
142
|
+
q: string;
|
|
143
|
+
/** true = positive (rule must be consulted) · false = near-miss (must not). */
|
|
144
|
+
expected: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** FNV-1a 32-bit — deterministic sampling key (no Math.random). */
|
|
148
|
+
export function fnv1a(s: string): number {
|
|
149
|
+
let h = 0x811c9dc5;
|
|
150
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
151
|
+
h ^= s.charCodeAt(i);
|
|
152
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
153
|
+
}
|
|
154
|
+
return h >>> 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Derive the eval suite from the routing-matrix fixtures: positive →
|
|
159
|
+
* expected [rule], near_miss → expected []. Only rules present in the
|
|
160
|
+
* catalogue are scored. Capped at `maxCases` by sorting on the FNV-1a hash
|
|
161
|
+
* of `rule|kind|prompt` (prompt as tiebreak) and taking the head — a
|
|
162
|
+
* deterministic pseudo-shuffle that spreads the sample across rules and
|
|
163
|
+
* both case kinds instead of favouring the alphabet.
|
|
164
|
+
*/
|
|
165
|
+
export function derive_rule_cases(
|
|
166
|
+
catalog: readonly RuleMeta[],
|
|
167
|
+
matrixDir: string = MATRIX_DIR,
|
|
168
|
+
maxCases: number = DEFAULT_MAX_CASES,
|
|
169
|
+
): RuleCase[] {
|
|
170
|
+
const known = new Set(catalog.map((r) => r.name));
|
|
171
|
+
let files: string[];
|
|
172
|
+
try {
|
|
173
|
+
files = fs.readdirSync(matrixDir).filter((n) => n.endsWith('.yaml'));
|
|
174
|
+
} catch {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
files.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
178
|
+
const cases: RuleCase[] = [];
|
|
179
|
+
for (const file of files) {
|
|
180
|
+
const doc = parseYaml(fs.readFileSync(path.join(matrixDir, file), 'utf-8')) as {
|
|
181
|
+
rule?: unknown;
|
|
182
|
+
positives?: Array<{ prompt?: unknown }>;
|
|
183
|
+
near_misses?: Array<{ prompt?: unknown }>;
|
|
184
|
+
} | null;
|
|
185
|
+
const rule = typeof doc?.rule === 'string' ? doc.rule : '';
|
|
186
|
+
if (!rule || !known.has(rule)) continue;
|
|
187
|
+
for (const [items, expected] of [
|
|
188
|
+
[doc?.positives, true],
|
|
189
|
+
[doc?.near_misses, false],
|
|
190
|
+
] as const) {
|
|
191
|
+
if (!Array.isArray(items)) continue;
|
|
192
|
+
for (const item of items) {
|
|
193
|
+
if (typeof item?.prompt === 'string' && item.prompt.trim()) {
|
|
194
|
+
cases.push({ rule, q: item.prompt, expected });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
cases.sort((a, b) => {
|
|
200
|
+
const ha = fnv1a(`${a.rule}|${a.expected ? 'pos' : 'near'}|${a.q}`);
|
|
201
|
+
const hb = fnv1a(`${b.rule}|${b.expected ? 'pos' : 'near'}|${b.q}`);
|
|
202
|
+
if (ha !== hb) return ha - hb;
|
|
203
|
+
return a.q < b.q ? -1 : a.q > b.q ? 1 : 0;
|
|
204
|
+
});
|
|
205
|
+
return cases.slice(0, Math.max(0, maxCases));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Eval ─────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
export interface RuleCaseResult {
|
|
211
|
+
rule: string;
|
|
212
|
+
q: string;
|
|
213
|
+
expected: boolean;
|
|
214
|
+
observed: boolean;
|
|
215
|
+
consulted: string[];
|
|
216
|
+
passed: boolean;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface RuleEvalResult {
|
|
220
|
+
mode: 'rules';
|
|
221
|
+
model: string;
|
|
222
|
+
timestamp: string;
|
|
223
|
+
router: string;
|
|
224
|
+
catalogue_size: number;
|
|
225
|
+
cases: RuleCaseResult[];
|
|
226
|
+
metrics: Metrics;
|
|
227
|
+
input_tokens: number;
|
|
228
|
+
output_tokens: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Run the derived suite through a router (awaiting routeAsync when present). */
|
|
232
|
+
export async function run_rule_eval(
|
|
233
|
+
cases: readonly RuleCase[],
|
|
234
|
+
router: AsyncCapableRouter,
|
|
235
|
+
catalog: readonly RuleMeta[],
|
|
236
|
+
model: string,
|
|
237
|
+
): Promise<RuleEvalResult> {
|
|
238
|
+
const result: RuleEvalResult = {
|
|
239
|
+
mode: 'rules',
|
|
240
|
+
model,
|
|
241
|
+
timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00'),
|
|
242
|
+
router: router.name,
|
|
243
|
+
catalogue_size: catalog.length,
|
|
244
|
+
cases: [],
|
|
245
|
+
metrics: compute_metrics([]),
|
|
246
|
+
input_tokens: 0,
|
|
247
|
+
output_tokens: 0,
|
|
248
|
+
};
|
|
249
|
+
for (const c of cases) {
|
|
250
|
+
const [consulted, inTok, outTok] =
|
|
251
|
+
typeof router.routeAsync === 'function'
|
|
252
|
+
? await router.routeAsync(c.q, [...catalog])
|
|
253
|
+
: router.route(c.q, [...catalog]);
|
|
254
|
+
const observed = consulted.includes(c.rule);
|
|
255
|
+
result.cases.push({
|
|
256
|
+
rule: c.rule,
|
|
257
|
+
q: c.q,
|
|
258
|
+
expected: c.expected,
|
|
259
|
+
observed,
|
|
260
|
+
consulted: [...consulted].sort(),
|
|
261
|
+
passed: observed === c.expected,
|
|
262
|
+
});
|
|
263
|
+
result.input_tokens += inTok;
|
|
264
|
+
result.output_tokens += outTok;
|
|
265
|
+
}
|
|
266
|
+
result.metrics = compute_metrics(
|
|
267
|
+
result.cases.map((c) => ({
|
|
268
|
+
q: c.q,
|
|
269
|
+
expected: c.expected,
|
|
270
|
+
observed: c.observed,
|
|
271
|
+
loaded_skills: c.consulted,
|
|
272
|
+
passed: c.passed,
|
|
273
|
+
})),
|
|
274
|
+
);
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── CLI ──────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
export interface RuleEvalOptions {
|
|
281
|
+
dryRun?: boolean;
|
|
282
|
+
model?: string;
|
|
283
|
+
out?: string;
|
|
284
|
+
maxCases?: number;
|
|
285
|
+
/** Injectable router (tests). Default: MockRouter (dry) / AnthropicFetchRouter (live). */
|
|
286
|
+
router?: AsyncCapableRouter;
|
|
287
|
+
routerJsonPath?: string;
|
|
288
|
+
rulesDir?: string;
|
|
289
|
+
matrixDir?: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface RuleEvalSummary {
|
|
293
|
+
result: RuleEvalResult;
|
|
294
|
+
dry_run: boolean;
|
|
295
|
+
floor_passed: boolean;
|
|
296
|
+
out: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function run_rules_mode(opts: RuleEvalOptions = {}): Promise<RuleEvalSummary> {
|
|
300
|
+
const dryRun = opts.dryRun ?? false;
|
|
301
|
+
const model = opts.model ?? DEFAULT_RULES_MODEL;
|
|
302
|
+
const out = opts.out ?? DEFAULT_OUT;
|
|
303
|
+
const catalog = build_rule_catalog(opts.routerJsonPath, opts.rulesDir);
|
|
304
|
+
const cases = derive_rule_cases(catalog, opts.matrixDir, opts.maxCases ?? DEFAULT_MAX_CASES);
|
|
305
|
+
if (cases.length === 0) {
|
|
306
|
+
throw new Error('rules mode: derived suite is empty (no routing-matrix cases)');
|
|
307
|
+
}
|
|
308
|
+
const router: AsyncCapableRouter =
|
|
309
|
+
opts.router ??
|
|
310
|
+
(dryRun
|
|
311
|
+
? new MockRouter((q, rules) =>
|
|
312
|
+
rules.filter((r) => q.toLowerCase().includes(r.name)).map((r) => r.name),
|
|
313
|
+
)
|
|
314
|
+
: (new AnthropicFetchRouter({
|
|
315
|
+
apiKey: loadKeyFromFile('anthropic.key'),
|
|
316
|
+
model,
|
|
317
|
+
promptHeader: RULES_PROMPT_HEADER,
|
|
318
|
+
}) as AsyncCapableRouter));
|
|
319
|
+
|
|
320
|
+
const result = await run_rule_eval(cases, router, catalog, model);
|
|
321
|
+
const floorPassed =
|
|
322
|
+
result.metrics.precision >= RULES_MODE_FLOOR.minPrecision &&
|
|
323
|
+
result.metrics.recall >= RULES_MODE_FLOOR.minRecall;
|
|
324
|
+
|
|
325
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
326
|
+
fs.writeFileSync(out, JSON.stringify(result, null, 2) + '\n', 'utf-8');
|
|
327
|
+
|
|
328
|
+
return { result, dry_run: dryRun, floor_passed: floorPassed, out };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parse_args(argv: string[]): RuleEvalOptions & { help?: boolean } {
|
|
332
|
+
const out: RuleEvalOptions & { help?: boolean } = {};
|
|
333
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
334
|
+
const a = argv[i] as string;
|
|
335
|
+
const take = (): string => {
|
|
336
|
+
const v = argv[++i];
|
|
337
|
+
if (v === undefined) {
|
|
338
|
+
process.stderr.write(`rule_trigger_eval: error: argument ${a}: expected one argument\n`);
|
|
339
|
+
process.exit(2);
|
|
340
|
+
}
|
|
341
|
+
return v as string;
|
|
342
|
+
};
|
|
343
|
+
if (a === '--dry-run') out.dryRun = true;
|
|
344
|
+
else if (a === '--model') out.model = take();
|
|
345
|
+
else if (a === '--out') out.out = path.resolve(take());
|
|
346
|
+
else if (a === '--max-cases') out.maxCases = Number(take());
|
|
347
|
+
else if (a === '-h' || a === '--help') out.help = true;
|
|
348
|
+
else {
|
|
349
|
+
process.stderr.write(`rule_trigger_eval: error: unrecognized arguments: ${a}\n`);
|
|
350
|
+
process.exit(2);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (out.maxCases !== undefined && (!Number.isInteger(out.maxCases) || out.maxCases <= 0)) {
|
|
354
|
+
process.stderr.write('rule_trigger_eval: error: --max-cases must be a positive integer\n');
|
|
355
|
+
process.exit(2);
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function main(argv?: string[]): Promise<number> {
|
|
361
|
+
const opts = parse_args(argv ?? process.argv.slice(2));
|
|
362
|
+
if (opts.help) {
|
|
363
|
+
process.stdout.write(
|
|
364
|
+
'usage: rule_trigger_eval [--dry-run] [--model MODEL] [--out FILE]\n' +
|
|
365
|
+
' [--max-cases N]\n',
|
|
366
|
+
);
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
let summary: RuleEvalSummary;
|
|
370
|
+
try {
|
|
371
|
+
summary = await run_rules_mode(opts);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
process.stderr.write(`❌ rule_trigger_eval: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
374
|
+
return 2;
|
|
375
|
+
}
|
|
376
|
+
const m = summary.result.metrics;
|
|
377
|
+
process.stdout.write(
|
|
378
|
+
`rules-mode trigger eval · ${summary.result.cases.length} cases over ` +
|
|
379
|
+
`${summary.result.catalogue_size} rules (kernel excluded)` +
|
|
380
|
+
`${summary.dry_run ? ' · DRY-RUN (floor advisory)' : ''}\n` +
|
|
381
|
+
` precision ${m.precision} (floor ${RULES_MODE_FLOOR.minPrecision}) · ` +
|
|
382
|
+
`recall ${m.recall} (floor ${RULES_MODE_FLOOR.minRecall}) · ${summary.out}\n`,
|
|
383
|
+
);
|
|
384
|
+
for (const c of summary.result.cases) {
|
|
385
|
+
if (c.passed) continue;
|
|
386
|
+
process.stdout.write(
|
|
387
|
+
` ❌ [${c.expected ? 'FN' : 'FP'}] ${c.rule} :: ${c.q}\n`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (!summary.floor_passed && !summary.dry_run) {
|
|
391
|
+
process.stderr.write(
|
|
392
|
+
'❌ rules-mode metrics below advisory floor — fails the scheduled canary job only, never a PR.\n',
|
|
393
|
+
);
|
|
394
|
+
return 1;
|
|
395
|
+
}
|
|
396
|
+
process.stdout.write('✅ rules-mode eval complete\n');
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const argvUrl = process.argv[1] === undefined ? '' : pathToFileURL(path.resolve(process.argv[1])).href;
|
|
401
|
+
if (import.meta.url === argvUrl) {
|
|
402
|
+
main().then((code) => process.exit(code));
|
|
403
|
+
}
|
|
@@ -74,7 +74,11 @@
|
|
|
74
74
|
"routes_to": {
|
|
75
75
|
"type": "array",
|
|
76
76
|
"items": {"type": "string", "pattern": "^(skill|guideline|command|contract):"},
|
|
77
|
-
"description": "Router targets (skill / guideline / command / contract). Forbidden on kernel rules. Schema: docs/contracts/rule-router.md."
|
|
77
|
+
"description": "Router targets (skill / guideline / command / contract). Forbidden on kernel rules. Non-kernel rules declare either routes_to or self_contained: true. Schema: docs/contracts/rule-router.md."
|
|
78
|
+
},
|
|
79
|
+
"self_contained": {
|
|
80
|
+
"type": "boolean",
|
|
81
|
+
"description": "Certifies a non-kernel rule whose body IS the constraint — a prohibition, gate, or output-format law with no procedure to delegate — so it legitimately declares no routes_to. May offload detail via load_context. Per-rule certification rationale lives in ADR-210's appendix; the linter errors when a non-kernel rule declares neither routes_to nor this marker. Schema: docs/contracts/rule-router.md."
|
|
78
82
|
},
|
|
79
83
|
"applies_to_user_types": {
|
|
80
84
|
"type": "array",
|
|
@@ -19,14 +19,25 @@
|
|
|
19
19
|
* new conversation on hook-capable hosts. Copilot (no hook surface) falls back
|
|
20
20
|
* to the rule alone.
|
|
21
21
|
*
|
|
22
|
-
* Gate
|
|
23
|
-
*
|
|
22
|
+
* Gate + name resolution (first non-empty wins — the canary is a PERSONAL,
|
|
23
|
+
* user-global concern; the project file is only an override):
|
|
24
|
+
*
|
|
25
|
+
* 1. `<workspace_root>/.agent-settings.yml` → `personal.canary_name`
|
|
26
|
+
* 2. `<event4u_root>/settings/.agent-settings.yml` → `personal.canary_name`
|
|
27
|
+
* (the wizard-managed user-global settings; legacy XDG root read as
|
|
28
|
+
* fallback via `user_global_paths.resolve_with_fallback`)
|
|
29
|
+
* 3. `<event4u_root>/settings/.agent-user.yml` → `identity.name`
|
|
30
|
+
* (the global user identity the setup wizard already collects — no
|
|
31
|
+
* per-project duplication of the name)
|
|
32
|
+
*
|
|
33
|
+
* No name anywhere → clean no-op (exit 0, no stdout). Never blocks
|
|
24
34
|
* (fail_closed: false).
|
|
25
35
|
*/
|
|
26
36
|
import fs from "node:fs";
|
|
27
37
|
import path from "node:path";
|
|
28
38
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29
39
|
import { readHookStdin } from "./hooks/hook_stdin.js";
|
|
40
|
+
import { resolve_with_fallback } from "./_lib/user_global_paths.js";
|
|
30
41
|
|
|
31
42
|
const EXIT_ALLOW = 0;
|
|
32
43
|
|
|
@@ -73,6 +84,86 @@ export function read_canary_name(settings_path: string): string | null {
|
|
|
73
84
|
return null;
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Line-walker for `identity.name` in the wizard's user-global
|
|
89
|
+
* `settings/.agent-user.yml` — same no-YAML-dependency approach as
|
|
90
|
+
* `read_canary_name`. Returns the raw scalar or null when absent.
|
|
91
|
+
*/
|
|
92
|
+
export function read_identity_name(user_yml_path: string): string | null {
|
|
93
|
+
let text: string;
|
|
94
|
+
try {
|
|
95
|
+
if (!fs.statSync(user_yml_path).isFile()) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
text = fs.readFileSync(user_yml_path, "utf-8");
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
let in_identity = false;
|
|
103
|
+
for (const line of text.split(/\r?\n/)) {
|
|
104
|
+
if (/^identity\s*:\s*(?:#.*)?$/.test(line)) {
|
|
105
|
+
in_identity = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (in_identity && /^\S/.test(line)) {
|
|
109
|
+
in_identity = false; // left the identity: block
|
|
110
|
+
}
|
|
111
|
+
if (in_identity) {
|
|
112
|
+
const m = /^\s+name\s*:\s*(.*?)\s*(?:#.*)?$/.exec(line);
|
|
113
|
+
if (m && m[1] !== undefined) {
|
|
114
|
+
return m[1].trim();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the canary name across the three layers (project override →
|
|
123
|
+
* user-global settings → user-global identity). First non-empty wins;
|
|
124
|
+
* an empty scalar at a layer means "not set here", not "feature off",
|
|
125
|
+
* so the walk continues to the next layer.
|
|
126
|
+
*/
|
|
127
|
+
/** Which settings layer decided the canary name — surfaced verbatim in the
|
|
128
|
+
* hook's `reason` so `routing:doctor` can answer "why did/didn't this gate
|
|
129
|
+
* fire" with the deciding layer named (road-to-tested-routing Phase 1). */
|
|
130
|
+
export type CanaryLayer =
|
|
131
|
+
| "project override"
|
|
132
|
+
| "user-global settings"
|
|
133
|
+
| "user-global identity"
|
|
134
|
+
| "none";
|
|
135
|
+
|
|
136
|
+
export interface CanaryResolution {
|
|
137
|
+
name: string;
|
|
138
|
+
layer: CanaryLayer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolve_canary_name(workspace_root: string): CanaryResolution {
|
|
142
|
+
const project = sanitize_name(
|
|
143
|
+
read_canary_name(path.join(workspace_root, SETTINGS_FILE)),
|
|
144
|
+
);
|
|
145
|
+
if (project) {
|
|
146
|
+
return { name: project, layer: "project override" };
|
|
147
|
+
}
|
|
148
|
+
const global_settings = resolve_with_fallback(
|
|
149
|
+
path.join("settings", ".agent-settings.yml"),
|
|
150
|
+
);
|
|
151
|
+
if (global_settings) {
|
|
152
|
+
const name = sanitize_name(read_canary_name(global_settings));
|
|
153
|
+
if (name) {
|
|
154
|
+
return { name, layer: "user-global settings" };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const global_user = resolve_with_fallback(path.join("settings", ".agent-user.yml"));
|
|
158
|
+
if (global_user) {
|
|
159
|
+
const name = sanitize_name(read_identity_name(global_user));
|
|
160
|
+
if (name) {
|
|
161
|
+
return { name, layer: "user-global identity" };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { name: "", layer: "none" };
|
|
165
|
+
}
|
|
166
|
+
|
|
76
167
|
/**
|
|
77
168
|
* The name is user-owned config, but it lands verbatim in model context —
|
|
78
169
|
* strip quotes, collapse whitespace, drop markup-significant characters, and
|
|
@@ -135,15 +226,15 @@ export function main(): number {
|
|
|
135
226
|
}
|
|
136
227
|
|
|
137
228
|
const root = _workspaceRoot(env);
|
|
138
|
-
const name =
|
|
229
|
+
const { name, layer } = resolve_canary_name(root);
|
|
139
230
|
if (!name) {
|
|
140
|
-
return EXIT_ALLOW; // canary not configured — clean no-op
|
|
231
|
+
return EXIT_ALLOW; // canary not configured on any layer — clean no-op
|
|
141
232
|
}
|
|
142
233
|
|
|
143
234
|
process.stdout.write(
|
|
144
235
|
`${JSON.stringify({
|
|
145
236
|
decision: "allow",
|
|
146
|
-
reason: `session-canary: active for "${name}"`,
|
|
237
|
+
reason: `session-canary: active for "${name}" (${layer})`,
|
|
147
238
|
context: build_canary_block(name),
|
|
148
239
|
})}\n`,
|
|
149
240
|
);
|
|
@@ -794,30 +794,6 @@ function extractFrontmatterField(frontmatter: string, pattern: RegExp): string |
|
|
|
794
794
|
|
|
795
795
|
type YamlListItem = string | Record<string, string>;
|
|
796
796
|
|
|
797
|
-
function _parseTrustLevel(frontmatter: string): string | null {
|
|
798
|
-
const lines = splitlines(frontmatter);
|
|
799
|
-
let inBlock = false;
|
|
800
|
-
for (const line of lines) {
|
|
801
|
-
if (!inBlock) {
|
|
802
|
-
if (line.startsWith('trust:')) {
|
|
803
|
-
const rhs = line.slice('trust:'.length).trim();
|
|
804
|
-
if (rhs === '') {
|
|
805
|
-
inBlock = true;
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
continue;
|
|
809
|
-
}
|
|
810
|
-
if (line.startsWith(' level:')) {
|
|
811
|
-
return stripQuotes(line.slice(' level:'.length).trim());
|
|
812
|
-
}
|
|
813
|
-
if (line.startsWith(' ')) {
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
return null;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
797
|
function stripQuotes(s: string): string {
|
|
822
798
|
return s.replace(/^["']/, '').replace(/["']$/, '');
|
|
823
799
|
}
|
|
@@ -932,13 +908,18 @@ function lint_router_frontmatter(
|
|
|
932
908
|
}
|
|
933
909
|
|
|
934
910
|
if (routesTo === null) {
|
|
935
|
-
|
|
936
|
-
|
|
911
|
+
// Self-contained carve-out (ADR-210): a rule whose body IS the
|
|
912
|
+
// constraint (prohibition / gate / format law — no procedure to
|
|
913
|
+
// delegate) certifies itself via `self_contained: true` instead of
|
|
914
|
+
// routing. Neither declared → error: the contract's "routes_to ≥ 1
|
|
915
|
+
// unless self-contained" is unenforceable without a marker.
|
|
916
|
+
const selfContained = /^self_contained:[ \t]*true[ \t]*$/m.test(frontmatter);
|
|
917
|
+
if (!selfContained) {
|
|
937
918
|
issues.push(
|
|
938
919
|
new Issue(
|
|
939
|
-
'
|
|
920
|
+
'error',
|
|
940
921
|
'router_routes_to_missing',
|
|
941
|
-
|
|
922
|
+
"Non-kernel rule declares neither routes_to: nor self_contained: true — route the body to its skill/guideline, or certify it self-contained per docs/contracts/rule-router.md",
|
|
942
923
|
),
|
|
943
924
|
);
|
|
944
925
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# scripts/smoke/skills.sh — skills-tier smoke (step-11 Phase 3 Step 5).
|
|
3
3
|
#
|
|
4
4
|
# Picks 5 random skills (deterministic seed = epoch day) from
|
|
5
|
-
#
|
|
5
|
+
# src/skills/*/SKILL.md and asserts:
|
|
6
6
|
# 1. SKILL.md exists.
|
|
7
7
|
# 2. Frontmatter parses + validates against scripts/schemas/skill.schema.json.
|
|
8
8
|
# 3. `name:` field matches the parent directory name.
|