@event4u/agent-config 9.16.0 → 9.18.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/AGENTS.md +1 -1
- package/CHANGELOG.md +80 -130
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/challenge-me/vision.md +5 -0
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
- package/dist/agent-src/commands/feature/plan.md +20 -0
- package/dist/agent-src/commands/feature/roadmap.md +19 -0
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/roadmap/create.md +29 -0
- package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/architecture.md +4 -2
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-consistency.md +10 -62
- package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
- package/dist/agent-src/rules/broken-access-control.md +4 -0
- package/dist/agent-src/rules/cli-output-handling.md +5 -0
- package/dist/agent-src/rules/code-comment-discipline.md +3 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/design-fidelity.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +4 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
- package/dist/agent-src/rules/improve-before-implement.md +4 -0
- package/dist/agent-src/rules/laravel-routing.md +4 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +5 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +3 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +4 -0
- package/dist/agent-src/rules/source-discovery-gate.md +3 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +5 -0
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/ui-audit-gate.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
- package/dist/agent-src/skills/design-review/SKILL.md +4 -141
- package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
- package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
- package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
- package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +13 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
- package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_explain.js +36 -36
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +1169 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +249 -0
- package/dist/cli-delegate/cmd_route_explain.js +285 -0
- 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_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +84 -84
- 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 +602 -507
- package/dist/install/install.mjs +34 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/schemas/settings.js +25 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/src/config/agent-settings.template.yml +68 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +70 -0
- package/src/scripts/_cli/cmd_explain.ts +41 -36
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/cmd_route_audit.ts +290 -0
- package/src/scripts/_cli/cmd_route_explain.ts +269 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +32 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/git_env.ts +40 -0
- package/src/scripts/_lib/md_table.ts +27 -0
- package/src/scripts/_lib/planning_settings.ts +43 -0
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/_lib/router_match.ts +301 -0
- package/src/scripts/annotate_r1_outcomes.ts +258 -0
- package/src/scripts/check_completion_review.ts +1140 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/compile_router.ts +20 -3
- package/src/scripts/dispatch_r2_reviewer.ts +968 -0
- package/src/scripts/eval_ui_triviality.ts +156 -0
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +327 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +931 -0
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/lint_trigger_collisions.ts +257 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/router_telemetry.ts +16 -290
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +41 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-source release material (release-truth Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* The recorded 9.14.0 failure: PR body, CHANGELOG entry, GitHub release
|
|
5
|
+
* body, and tag metadata were generated at different times from different
|
|
6
|
+
* scopes — the PR body said 10,054 tests while the merged changelog carried
|
|
7
|
+
* 10,056 and more items. The fix is structural: the CHANGELOG section at the
|
|
8
|
+
* relevant head is the ONE final source, and every other surface is a pure
|
|
9
|
+
* derivation of it (release.ts re-derives on branch update / resume / merge,
|
|
10
|
+
* so late commits cannot desynchronize the surfaces).
|
|
11
|
+
*
|
|
12
|
+
* Everything here is a pure function of strings so the equality gate
|
|
13
|
+
* (`check_release_surface_equality.ts`) and the fixture tests exercise the
|
|
14
|
+
* exact production code paths.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface ChangelogSection {
|
|
18
|
+
/** The `## …` heading line, verbatim. */
|
|
19
|
+
heading: string;
|
|
20
|
+
/** Section content below the heading, outer blank lines stripped. */
|
|
21
|
+
body: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const _NEXT_HEADING_RE = /^##\s+\[?\d+\.\d+\.\d+/m;
|
|
25
|
+
|
|
26
|
+
function _reEscape(s: string): string {
|
|
27
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract the CHANGELOG section for `version` — heading plus body up to the
|
|
32
|
+
* next version heading (or end of file). Returns null when no heading for
|
|
33
|
+
* `version` exists. Matches both heading shapes the generator emits
|
|
34
|
+
* (`## X.Y.Z (date)` and `## [X.Y.Z](compare-url) (date)`).
|
|
35
|
+
*/
|
|
36
|
+
export function extract_changelog_section(text: string, version: string): ChangelogSection | null {
|
|
37
|
+
const headingRe = new RegExp(`^##\\s+\\[?${_reEscape(version)}\\b.*$`, 'm');
|
|
38
|
+
const m = headingRe.exec(text);
|
|
39
|
+
if (!m) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const bodyStart = m.index + m[0].length;
|
|
43
|
+
const rest = text.slice(bodyStart);
|
|
44
|
+
const next = _NEXT_HEADING_RE.exec(rest);
|
|
45
|
+
const sectionEnd = bodyStart + (next ? next.index : rest.length);
|
|
46
|
+
const body = text
|
|
47
|
+
.slice(bodyStart, sectionEnd)
|
|
48
|
+
.replace(/^\n+/u, '')
|
|
49
|
+
.replace(/\s+$/u, '');
|
|
50
|
+
return { heading: m[0], body };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Footer appended to the release-PR body; the strip helper accepts legacy variants. */
|
|
54
|
+
export const PR_BODY_FOOTER = 'Created by `src/scripts/release.ts`.';
|
|
55
|
+
const _LEGACY_PR_FOOTERS = ['Created by `scripts/release.py`.', PR_BODY_FOOTER];
|
|
56
|
+
|
|
57
|
+
/** Marker `_cap_body` embeds when a surface was truncated to a GitHub limit. */
|
|
58
|
+
export const TRUNCATION_MARKER = '_Changelog truncated to fit GitHub';
|
|
59
|
+
|
|
60
|
+
/** Release-PR body derived from the (possibly capped) changelog-section body. */
|
|
61
|
+
export function pr_body_from_section(cappedBody: string, version: string): string {
|
|
62
|
+
return `Release ${version}.\n\n${cappedBody}\n\n${PR_BODY_FOOTER}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** GitHub-release notes derived from the (possibly capped) changelog-section body. */
|
|
66
|
+
export function release_notes_from_section(cappedBody: string, version: string): string {
|
|
67
|
+
return cappedBody || `Release ${version}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Annotated-tag message derived from the merged changelog-section body. */
|
|
71
|
+
export function tag_message_from_section(body: string, version: string): string {
|
|
72
|
+
const trailer = body ? `\n\n${body}` : '';
|
|
73
|
+
return `release: ${version}${trailer}\n`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Inverse of `pr_body_from_section`: return the changelog-derived middle of a
|
|
78
|
+
* release-PR body, or null when the body does not carry the canonical
|
|
79
|
+
* `Release X.Y.Z.` prefix (then it was not generated by this pipeline).
|
|
80
|
+
*/
|
|
81
|
+
export function strip_pr_wrapper(prBody: string, version: string): string | null {
|
|
82
|
+
const prefix = `Release ${version}.`;
|
|
83
|
+
let s = prBody.replace(/\r\n/gu, '\n').trim();
|
|
84
|
+
if (!s.startsWith(prefix)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
s = s.slice(prefix.length);
|
|
88
|
+
for (const footer of _LEGACY_PR_FOOTERS) {
|
|
89
|
+
if (s.trimEnd().endsWith(footer)) {
|
|
90
|
+
s = s.trimEnd();
|
|
91
|
+
s = s.slice(0, s.length - footer.length);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return s.replace(/^\n+/u, '').replace(/\s+$/u, '');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whitespace/anchor normalization — deliberately nothing stronger, per the
|
|
100
|
+
* roadmap lock ("not 'similar', equal"): CRLF → LF, trailing whitespace per
|
|
101
|
+
* line dropped, runs of blank lines collapsed to one, outer blank lines
|
|
102
|
+
* trimmed. Content differences always survive normalization.
|
|
103
|
+
*/
|
|
104
|
+
export function normalize_release_text(s: string): string {
|
|
105
|
+
return s
|
|
106
|
+
.replace(/\r\n/gu, '\n')
|
|
107
|
+
.split('\n')
|
|
108
|
+
.map((line) => line.replace(/[ \t]+$/u, ''))
|
|
109
|
+
.join('\n')
|
|
110
|
+
.replace(/\n{3,}/gu, '\n\n')
|
|
111
|
+
.replace(/^\n+/u, '')
|
|
112
|
+
.replace(/\n+$/u, '');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compare a derived release surface against the changelog-section body it
|
|
117
|
+
* must equal. Returns null when equal (after normalization), else a
|
|
118
|
+
* human-readable first-divergence report. A truncated surface (carrying the
|
|
119
|
+
* `_cap_body` notice) is compared as a prefix of the changelog side — the
|
|
120
|
+
* truncation itself is not a divergence.
|
|
121
|
+
*/
|
|
122
|
+
export function surface_divergence(surfaceText: string, changelogBody: string): string | null {
|
|
123
|
+
const a = normalize_release_text(surfaceText);
|
|
124
|
+
const b = normalize_release_text(changelogBody);
|
|
125
|
+
if (a.includes(TRUNCATION_MARKER)) {
|
|
126
|
+
const cut = a.indexOf(TRUNCATION_MARKER);
|
|
127
|
+
const prefix = a
|
|
128
|
+
.slice(0, cut)
|
|
129
|
+
.replace(/>\s*$/u, '')
|
|
130
|
+
.replace(/\n+$/u, '');
|
|
131
|
+
if (b.startsWith(prefix)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return _first_diff(prefix, b.slice(0, prefix.length));
|
|
135
|
+
}
|
|
136
|
+
if (a === b) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return _first_diff(a, b);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _first_diff(a: string, b: string): string {
|
|
143
|
+
const al = a.split('\n');
|
|
144
|
+
const bl = b.split('\n');
|
|
145
|
+
const n = Math.max(al.length, bl.length);
|
|
146
|
+
for (let i = 0; i < n; i++) {
|
|
147
|
+
if (al[i] !== bl[i]) {
|
|
148
|
+
return (
|
|
149
|
+
`first divergence at line ${i + 1}: ` +
|
|
150
|
+
`surface=${JSON.stringify(al[i] ?? '<missing>')} ` +
|
|
151
|
+
`changelog=${JSON.stringify(bl[i] ?? '<missing>')}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return 'surfaces differ only in normalization-invisible content';
|
|
156
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE single trigger-matcher implementation for `dist/router.json`.
|
|
3
|
+
*
|
|
4
|
+
* Parity by construction: every surface that answers "which rules fire on
|
|
5
|
+
* this prompt?" — router replay (`router_telemetry.ts`), trigger-coverage
|
|
6
|
+
* CI (`trigger_coverage.ts`), `agent-config explain route`, and
|
|
7
|
+
* `agent-config route:explain` — MUST import the matchers from this module.
|
|
8
|
+
* A second implementation of these semantics anywhere in the tree is a
|
|
9
|
+
* violation (enforced by `tests/scripts/router_match_parity.test.ts`); the
|
|
10
|
+
* historical divergence this kills was `cmd_explain.ts` shipping unanchored
|
|
11
|
+
* keyword matching while the replay path was anchored.
|
|
12
|
+
*
|
|
13
|
+
* Canonical semantics: `docs/contracts/rule-router.md` § `triggers:` shape
|
|
14
|
+
* (the trigger-semantics table — anchored `keyword`, unanchored `phrase`,
|
|
15
|
+
* `command` prefix, `path_prefix` / `file_pattern` over open files).
|
|
16
|
+
*
|
|
17
|
+
* Extracted verbatim from `router_telemetry.ts` (which re-exports the same
|
|
18
|
+
* names for existing importers). Behaviour is pinned by the routing-matrix
|
|
19
|
+
* and trigger-coverage suites — do not "fix" quirks here.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type JsonValue =
|
|
23
|
+
| string
|
|
24
|
+
| number
|
|
25
|
+
| boolean
|
|
26
|
+
| null
|
|
27
|
+
| JsonValue[]
|
|
28
|
+
| { [key: string]: JsonValue };
|
|
29
|
+
|
|
30
|
+
export type Trigger = Record<string, JsonValue>;
|
|
31
|
+
|
|
32
|
+
export interface Rule {
|
|
33
|
+
id?: JsonValue;
|
|
34
|
+
triggers?: JsonValue;
|
|
35
|
+
[key: string]: JsonValue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Router {
|
|
39
|
+
kernel?: JsonValue;
|
|
40
|
+
tier_1?: JsonValue;
|
|
41
|
+
tier_2?: JsonValue;
|
|
42
|
+
[key: string]: JsonValue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── fnmatch (Python fnmatch.fnmatch) ─────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Translate a Unix shell pattern into a RegExp, matching CPython's
|
|
49
|
+
* `fnmatch.translate`. `*` → `.*`, `?` → `.`, `[...]` char classes
|
|
50
|
+
* preserved (with `!` → `^` negation), everything else escaped.
|
|
51
|
+
* Matching is case-sensitive on POSIX (the corpus paths are POSIX).
|
|
52
|
+
*/
|
|
53
|
+
function _fnmatchToRegExp(pat: string): RegExp {
|
|
54
|
+
let res = "";
|
|
55
|
+
let i = 0;
|
|
56
|
+
const n = pat.length;
|
|
57
|
+
while (i < n) {
|
|
58
|
+
const c = pat[i] as string;
|
|
59
|
+
i += 1;
|
|
60
|
+
if (c === "*") {
|
|
61
|
+
res += ".*";
|
|
62
|
+
} else if (c === "?") {
|
|
63
|
+
res += ".";
|
|
64
|
+
} else if (c === "[") {
|
|
65
|
+
let j = i;
|
|
66
|
+
if (j < n && pat[j] === "!") {
|
|
67
|
+
j += 1;
|
|
68
|
+
}
|
|
69
|
+
if (j < n && pat[j] === "]") {
|
|
70
|
+
j += 1;
|
|
71
|
+
}
|
|
72
|
+
while (j < n && pat[j] !== "]") {
|
|
73
|
+
j += 1;
|
|
74
|
+
}
|
|
75
|
+
if (j >= n) {
|
|
76
|
+
res += "\\[";
|
|
77
|
+
} else {
|
|
78
|
+
let stuff = pat.slice(i, j);
|
|
79
|
+
if (!stuff.includes("-")) {
|
|
80
|
+
stuff = stuff.replace(/\\/g, "\\\\");
|
|
81
|
+
} else {
|
|
82
|
+
// Faithful port of CPython's dash-handling in fnmatch.translate:
|
|
83
|
+
// split the class body on `-` (bounded to [i, j)), escaping each
|
|
84
|
+
// chunk, then re-join with `-` so ranges survive.
|
|
85
|
+
const chunks: string[] = [];
|
|
86
|
+
let k = pat[i] === "!" ? i + 2 : i + 1;
|
|
87
|
+
let start = i;
|
|
88
|
+
while (true) {
|
|
89
|
+
const rel = pat.slice(k, j).indexOf("-");
|
|
90
|
+
if (rel < 0) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const idx = k + rel;
|
|
94
|
+
chunks.push(pat.slice(start, idx));
|
|
95
|
+
start = idx + 1;
|
|
96
|
+
k = idx + 3;
|
|
97
|
+
}
|
|
98
|
+
chunks.push(pat.slice(start, j));
|
|
99
|
+
stuff = chunks
|
|
100
|
+
.map((s) => s.replace(/\\/g, "\\\\").replace(/-/g, "\\-"))
|
|
101
|
+
.join("-");
|
|
102
|
+
}
|
|
103
|
+
// Escape regex set-meta chars except backslash/dash already handled.
|
|
104
|
+
stuff = stuff.replace(/([&~|])/g, "\\$1");
|
|
105
|
+
i = j + 1;
|
|
106
|
+
if (stuff.startsWith("!")) {
|
|
107
|
+
stuff = "^" + stuff.slice(1);
|
|
108
|
+
} else if (stuff.startsWith("^") || stuff.startsWith("[")) {
|
|
109
|
+
stuff = "\\" + stuff;
|
|
110
|
+
}
|
|
111
|
+
res += `[${stuff}]`;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
res += c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return new RegExp(`^(?:${res})$`, "s");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function _fnmatch(name: string, pat: string): boolean {
|
|
121
|
+
return _fnmatchToRegExp(pat).test(name);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Trigger matching ─────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/** Apply one trigger to a prompt + context; return True on match. */
|
|
127
|
+
/** Unicode word character — letters, digits, underscore (incl. umlauts). */
|
|
128
|
+
const _WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
129
|
+
|
|
130
|
+
function _isWordChar(c: string | undefined): boolean {
|
|
131
|
+
return c !== undefined && _WORD_CHAR.test(c);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Word-boundary-anchored keyword match (road-to-tested-routing Phase 3).
|
|
136
|
+
*
|
|
137
|
+
* A `keyword` occurrence counts only when its word-character edges sit on
|
|
138
|
+
* word boundaries: `ac` no longer fires inside "black", `plan` no longer
|
|
139
|
+
* fires inside "planet". Two deliberate reliefs:
|
|
140
|
+
*
|
|
141
|
+
* - An edge that is itself a NON-word character (e.g. `__()`, `/image:`,
|
|
142
|
+
* `trans(`, an emoji) carries no boundary requirement on that side —
|
|
143
|
+
* punctuation-shaped keywords keep their exact substring semantics.
|
|
144
|
+
* - The right boundary accepts one optional plural `s` ("icons", "options",
|
|
145
|
+
* "secrets" still fire their singular keyword). Richer inflection (German
|
|
146
|
+
* verb endings: "implementiere", "committen") is a DOCUMENTED accepted
|
|
147
|
+
* recall cost of anchoring — the matrices carry standalone-token phrasings
|
|
148
|
+
* for those languages instead.
|
|
149
|
+
*
|
|
150
|
+
* `phrase` deliberately stays unanchored substring; since this change the
|
|
151
|
+
* two trigger kinds genuinely differ.
|
|
152
|
+
*/
|
|
153
|
+
export function keyword_matches_anchored(prompt_lower: string, kw_lower: string): boolean {
|
|
154
|
+
if (kw_lower === "") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const needLeft = _isWordChar(kw_lower[0]);
|
|
158
|
+
const needRight = _isWordChar(kw_lower[kw_lower.length - 1]);
|
|
159
|
+
let idx = prompt_lower.indexOf(kw_lower);
|
|
160
|
+
while (idx !== -1) {
|
|
161
|
+
const leftOk = !needLeft || !_isWordChar(prompt_lower[idx - 1]);
|
|
162
|
+
let rightOk = true;
|
|
163
|
+
if (needRight) {
|
|
164
|
+
let end = idx + kw_lower.length;
|
|
165
|
+
if (prompt_lower[end] === "s") {
|
|
166
|
+
end += 1; // optional plural `s`
|
|
167
|
+
}
|
|
168
|
+
rightOk = !_isWordChar(prompt_lower[end]);
|
|
169
|
+
}
|
|
170
|
+
if (leftOk && rightOk) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
idx = prompt_lower.indexOf(kw_lower, idx + 1);
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function trigger_matches(
|
|
179
|
+
trigger: Trigger,
|
|
180
|
+
prompt: string,
|
|
181
|
+
open_files?: Iterable<string> | null,
|
|
182
|
+
command?: string | null,
|
|
183
|
+
): boolean {
|
|
184
|
+
const prompt_lower = prompt.toLowerCase();
|
|
185
|
+
if ("keyword" in trigger) {
|
|
186
|
+
return keyword_matches_anchored(prompt_lower, String(trigger["keyword"]).toLowerCase());
|
|
187
|
+
}
|
|
188
|
+
if ("phrase" in trigger) {
|
|
189
|
+
return prompt_lower.includes(String(trigger["phrase"]).toLowerCase());
|
|
190
|
+
}
|
|
191
|
+
if ("command" in trigger) {
|
|
192
|
+
if (!command) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return command.startsWith(String(trigger["command"]));
|
|
196
|
+
}
|
|
197
|
+
if ("path_prefix" in trigger) {
|
|
198
|
+
if (!open_files) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const pref = String(trigger["path_prefix"]);
|
|
202
|
+
for (const p of open_files) {
|
|
203
|
+
if (String(p).startsWith(pref)) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if ("file_pattern" in trigger) {
|
|
210
|
+
if (!open_files) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const pat = String(trigger["file_pattern"]);
|
|
214
|
+
for (const p of open_files) {
|
|
215
|
+
if (_fnmatch(String(p), pat)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface MatchedTrigger {
|
|
225
|
+
tier: string;
|
|
226
|
+
rule: JsonValue | undefined;
|
|
227
|
+
trigger: Trigger;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface ActivatedRule {
|
|
231
|
+
tier: string;
|
|
232
|
+
rule: JsonValue | undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface MatchResult {
|
|
236
|
+
matched_triggers: MatchedTrigger[];
|
|
237
|
+
activated_rules: ActivatedRule[];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function _asRuleList(value: JsonValue | undefined): Rule[] {
|
|
241
|
+
if (!Array.isArray(value)) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
return value as unknown as Rule[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function _asTriggerList(value: JsonValue | undefined): Trigger[] {
|
|
248
|
+
if (!Array.isArray(value)) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
return value as unknown as Trigger[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Return the matched-triggers + activated-rules for one prompt.
|
|
256
|
+
*
|
|
257
|
+
* Kernel rules are always active. tier_1 always considered. tier_2
|
|
258
|
+
* only considered when `profile == 'full'`.
|
|
259
|
+
*/
|
|
260
|
+
export function match_prompt(
|
|
261
|
+
router: Router,
|
|
262
|
+
prompt: string,
|
|
263
|
+
profile = "full",
|
|
264
|
+
open_files?: Iterable<string> | null,
|
|
265
|
+
command?: string | null,
|
|
266
|
+
): MatchResult {
|
|
267
|
+
const tiers: Array<[string, Rule[]]> = [
|
|
268
|
+
["tier_1", _asRuleList(router["tier_1"])],
|
|
269
|
+
];
|
|
270
|
+
if (profile === "full") {
|
|
271
|
+
tiers.push(["tier_2", _asRuleList(router["tier_2"])]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const matched_triggers: MatchedTrigger[] = [];
|
|
275
|
+
const activated_rules: ActivatedRule[] = [];
|
|
276
|
+
|
|
277
|
+
for (const [tier_name, rules] of tiers) {
|
|
278
|
+
for (const rule of rules) {
|
|
279
|
+
const rule_id = rule["id"];
|
|
280
|
+
const rule_triggers = _asTriggerList(rule["triggers"]);
|
|
281
|
+
let rule_hit = false;
|
|
282
|
+
for (const trig of rule_triggers) {
|
|
283
|
+
if (trigger_matches(trig, prompt, open_files, command)) {
|
|
284
|
+
matched_triggers.push({ tier: tier_name, rule: rule_id, trigger: trig });
|
|
285
|
+
rule_hit = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (rule_hit) {
|
|
289
|
+
activated_rules.push({ tier: tier_name, rule: rule_id });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Kernel rules are always active.
|
|
295
|
+
const kernel = Array.isArray(router["kernel"]) ? router["kernel"] : [];
|
|
296
|
+
for (const kid of kernel) {
|
|
297
|
+
activated_rules.push({ tier: "kernel", rule: kid });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { matched_triggers, activated_rules };
|
|
301
|
+
}
|