@event4u/agent-config 8.7.0 → 8.9.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 +109 -222
- package/CONTRIBUTING.md +25 -0
- package/README.md +7 -4
- package/dist/agent-src/commands/agent-handoff.md +39 -2
- package/dist/agent-src/commands/research/deep.md +8 -0
- package/dist/agent-src/commands/research/report.md +7 -0
- package/dist/agent-src/commands/roadmap/create.md +12 -0
- package/dist/agent-src/commands/roadmap/process-full.md +31 -5
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +36 -5
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +37 -0
- package/dist/agent-src/contexts/execution/subagent-topologies.md +34 -0
- package/dist/agent-src/contexts/execution/verification-mechanics.md +9 -9
- package/dist/agent-src/rules/active-remediation.md +92 -0
- package/dist/agent-src/rules/agent-authority.md +1 -1
- package/dist/agent-src/rules/broken-access-control.md +106 -0
- package/dist/agent-src/rules/communication-through-line.md +42 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
- package/dist/agent-src/rules/delegation-policy.md +5 -0
- package/dist/agent-src/rules/direct-answers.md +1 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +65 -0
- package/dist/agent-src/rules/git-history-discipline.md +18 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +6 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +66 -1
- package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
- package/dist/agent-src/rules/question-not-instruction.md +76 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/tool-safety.md +8 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +87 -22
- package/dist/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +19 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +112 -0
- package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
- package/dist/agent-src/skills/code-review/SKILL.md +2 -0
- package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
- package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
- package/dist/agent-src/skills/design-review/SKILL.md +29 -15
- package/dist/agent-src/skills/design-system-capture/SKILL.md +63 -24
- package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
- package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -0
- package/dist/agent-src/skills/mcp/SKILL.md +24 -0
- package/dist/agent-src/skills/mcp-builder/SKILL.md +9 -1
- package/dist/agent-src/skills/memory-consolidation/SKILL.md +55 -4
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +46 -5
- package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/secrets-management/SKILL.md +7 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +12 -3
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +112 -0
- package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +72 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
- package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +40 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +60 -0
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +268 -46
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +18 -8
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/install/install.mjs +141 -3
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_builtin_names.js +145 -0
- package/dist/scripts/_lib/claude_builtin_names.js.map +1 -0
- package/dist/scripts/condense.js +14 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +58 -2
- package/docs/architecture.md +3 -3
- package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
- package/docs/benchmark.md +113 -0
- package/docs/catalog.md +32 -5
- package/docs/contracts/command-clusters.md +18 -0
- package/docs/contracts/design-artifact-verification.md +124 -0
- package/docs/contracts/injected-block-authenticity-model.md +66 -0
- package/docs/contracts/mcp-tool-inventory.md +31 -30
- package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -2
- package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/experiences/content_creator.md +2 -0
- package/docs/experiences/finance.md +2 -0
- package/docs/experiences/founder.md +2 -0
- package/docs/experiences/ops.md +2 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -0
- package/docs/guidelines/agent-infra/emphasis-budget.md +45 -0
- package/docs/guidelines/agent-infra/memory-access.md +12 -0
- package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +151 -0
- package/docs/guidelines/agent-infra/tool-description-as-policy.md +35 -0
- package/docs/guidelines/design-canon.md +1 -0
- package/docs/proof.md +60 -3
- package/docs/release-runbook.md +138 -0
- package/docs/second-brain-scope.md +116 -0
- package/docs/succession.md +80 -0
- package/package.json +2 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/src/config/agent-settings.template.yml +25 -6
- package/src/scripts/_dispatch.bash +27 -0
- package/src/scripts/_lib/claude_builtin_names.ts +148 -0
- package/src/scripts/_lib/file_slicer.ts +117 -0
- package/src/scripts/_lib/lexical_index.ts +155 -0
- package/src/scripts/_lib/orchestration_gate.ts +12 -8
- package/src/scripts/_lib/orchestration_record.ts +170 -0
- package/src/scripts/_lib/orchestration_savings.ts +180 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +71 -0
- package/src/scripts/_lib/security_lint.ts +1 -0
- package/src/scripts/_lib/stat_index.ts +98 -0
- package/src/scripts/benchmark.ts +122 -0
- package/src/scripts/build_proof.ts +51 -0
- package/src/scripts/chat_history.ts +79 -7
- package/src/scripts/check_memory.ts +2 -0
- package/src/scripts/check_quality_regression.ts +51 -0
- package/src/scripts/check_roadmap_trackable.ts +3 -2
- package/src/scripts/condense.ts +14 -0
- package/src/scripts/discovery_graph.ts +0 -0
- package/src/scripts/domain_soundness_status.ts +236 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_index.ts +17 -0
- package/src/scripts/hot_context_hook.ts +47 -2
- package/src/scripts/install-hooks.sh +25 -0
- package/src/scripts/install.ts +35 -2
- package/src/scripts/learning_sidecar.ts +0 -0
- package/src/scripts/lint_agent_skill_names.ts +29 -0
- package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
- package/src/scripts/lint_knowledge_scale.ts +4 -3
- package/src/scripts/lint_roadmap_complexity.ts +124 -0
- package/src/scripts/lint_versioned_cache.ts +195 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +23 -3
- package/src/scripts/mcp_server/tools.ts +182 -1
- package/src/scripts/measure_lexical_ranking.ts +173 -0
- package/src/scripts/memory_lookup.ts +0 -0
- package/src/scripts/memory_replay.ts +285 -0
- package/src/scripts/orchestration_record.ts +140 -0
- package/src/scripts/orchestration_savings_report.ts +124 -0
- package/src/scripts/schemas/domain-truth.schema.json +94 -0
- package/src/scripts/schemas/evals.schema.json +113 -0
- package/src/scripts/schemas/skill.schema.json +4 -0
- package/src/scripts/score_domain_truth.ts +192 -0
- package/src/scripts/second_brain_retrieval.ts +278 -0
- package/src/scripts/second_brain_run.ts +341 -0
- package/src/scripts/second_brain_score.ts +186 -0
- package/src/scripts/session_memory_index.ts +94 -0
- package/src/scripts/skill_eval_coverage.ts +318 -0
- package/src/server/schemas/settings.ts +5 -2
|
@@ -26,6 +26,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
26
26
|
import { REPO, LEDGER_REL, load_ledger, pointer_unresolved } from './check_claims.js';
|
|
27
27
|
import { collectSkillGaps } from './check_skill_gaps.js';
|
|
28
28
|
import { loadRows as loadComparisonRows } from './check_comparison.js';
|
|
29
|
+
import { computeStatus as domainSoundnessStatus } from './domain_soundness_status.js';
|
|
29
30
|
|
|
30
31
|
const _FILE = fileURLToPath(import.meta.url);
|
|
31
32
|
const OUT_REL = 'docs/proof.md';
|
|
@@ -107,6 +108,54 @@ function render(): string {
|
|
|
107
108
|
L.push('Benchmark results are published under `docs/` including measured nulls.');
|
|
108
109
|
}
|
|
109
110
|
L.push('');
|
|
111
|
+
L.push('**Behavioural-eval coverage — the honest baseline.** Skill *quality* is only');
|
|
112
|
+
L.push('as good as its measurement. Today **2 of 264** skills carry a behavioural');
|
|
113
|
+
L.push('`evals.json`, and the highest-traffic / highest-cost tiers (default-surface +');
|
|
114
|
+
L.push('`rich` + routers, **0 of 35**) are not yet covered. We publish that gap rather');
|
|
115
|
+
L.push('than imply "264 evaluated skills": coverage is measured per tier');
|
|
116
|
+
L.push('(`./scripts-run src/scripts/skill_eval_coverage`) and **CI-ratcheted so it can');
|
|
117
|
+
L.push('only rise** — a merged change can never lower it. Authoring the priority-tier');
|
|
118
|
+
L.push('evals is gated on per-case human ratification (a generated assertion that');
|
|
119
|
+
L.push('checks the wrong property is worse than none), so the number grows');
|
|
120
|
+
L.push('deliberately, not overnight.');
|
|
121
|
+
L.push('');
|
|
122
|
+
const ds = domainSoundnessStatus();
|
|
123
|
+
L.push('**Non-coding domain soundness — scoped, not proven.** The `finance` /');
|
|
124
|
+
L.push('`founder` / `ops` / `content` profiles sell concrete domain value (DCF,');
|
|
125
|
+
L.push('runway, RICE, incident command, messaging), but the skills are forged on');
|
|
126
|
+
L.push('TS/PHP — "promising, not proven" off those stacks. A disclaimer floor');
|
|
127
|
+
L.push('bounds *liability*, not *correctness*: a skill can be format-correct,');
|
|
128
|
+
L.push(`disclaimered, and still embed a wrong domain assumption. Today **${ds.validated} of ${ds.total}**`);
|
|
129
|
+
L.push('default-surface domain skills carry a sourced `domain-truth` fixture');
|
|
130
|
+
L.push('(`./scripts-run src/scripts/domain_soundness_status`); the rest are labeled');
|
|
131
|
+
L.push('`unvalidated` and the validated count is CI-ratcheted. The fixtures landed');
|
|
132
|
+
L.push('so far are the **deterministic** targets (runway, unit-economics, DCF,');
|
|
133
|
+
L.push('forecasting, scenario band/sensitivity), whose answer keys are computed');
|
|
134
|
+
L.push('from cited standard formulas — never the skill\'s own output; the');
|
|
135
|
+
L.push('**rubric** targets (incident command, messaging, fundraising, editorial)');
|
|
136
|
+
L.push('need domain-competent grounding and remain unvalidated, so validation');
|
|
137
|
+
L.push('lands deliberately — the gap is published, never implied away.');
|
|
138
|
+
L.push('');
|
|
139
|
+
L.push('**Second-brain substrate — measured recall lift, honestly bounded.** On a');
|
|
140
|
+
L.push('deterministic multi-session recall corpus, the memory substrate beats a');
|
|
141
|
+
L.push('no-memory baseline AND an equal-byte placebo: memory-on **27/27** vs');
|
|
142
|
+
L.push('no-memory **10/27** vs placebo **9/27** (claude-haiku-4-5, 9 tasks × 3');
|
|
143
|
+
L.push('seeds, sign test **p = 0.031** for both pairings) — a real,');
|
|
144
|
+
L.push('placebo-controlled lift (`internal/bench/reports/second-brain-delta.json`).');
|
|
145
|
+
L.push('Scoped, not oversold: this is the context-value upper bound (perfect');
|
|
146
|
+
L.push('retrieval on a one-fact-per-task corpus), NOT retrieval precision under a');
|
|
147
|
+
L.push('large store; the lift concentrates exactly where memory is the only source');
|
|
148
|
+
L.push('and ties where the prompt self-contains the fact. Boundary vs a human PKM,');
|
|
149
|
+
L.push('and why the Obsidian export stays declined, are in');
|
|
150
|
+
L.push('[`docs/second-brain-scope.md`](second-brain-scope.md).');
|
|
151
|
+
L.push('');
|
|
152
|
+
L.push('A follow-up removed the perfect-retrieval assumption: against a store of');
|
|
153
|
+
L.push('keyword-overlapping confusers the REAL retrieval recalls the needed');
|
|
154
|
+
L.push('decision into the top-5 (9/9) and the model disambiguates it — retrieval-on');
|
|
155
|
+
L.push('**27/27** vs no-memory **5/27** and vs placebo **5/27** (p=0.008 both). The');
|
|
156
|
+
L.push('honest limit: the keyword scorer recalls but does not rank (mean tie-set');
|
|
157
|
+
L.push('3.3), which is the SQLite-FTS5 activation signal (ADR-116) at scale.');
|
|
158
|
+
L.push('');
|
|
110
159
|
L.push('## 3. Known limits (published, witness-tested)');
|
|
111
160
|
L.push('');
|
|
112
161
|
const skillGaps = collectSkillGaps();
|
|
@@ -164,6 +213,8 @@ function render(): string {
|
|
|
164
213
|
L.push('task check-refs # no broken internal references');
|
|
165
214
|
L.push('task check-skill-gaps # every logged known-limit cites a real witness test');
|
|
166
215
|
L.push('task check-comparison # every comparison-table "our evidence" pointer resolves');
|
|
216
|
+
L.push('./scripts-run src/scripts/skill_eval_coverage # behavioural-eval coverage, per tier');
|
|
217
|
+
L.push('./scripts-run src/scripts/skill_eval_coverage --check # the ratchet: coverage may not drop');
|
|
167
218
|
L.push('task build-proof-check # this page is in sync with its sources');
|
|
168
219
|
L.push('```');
|
|
169
220
|
L.push('');
|
|
@@ -43,6 +43,8 @@ import * as fs from 'node:fs';
|
|
|
43
43
|
import * as path from 'node:path';
|
|
44
44
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
45
45
|
|
|
46
|
+
import { _entry_tokens_estimate } from './memory_lookup.js';
|
|
47
|
+
|
|
46
48
|
import { load_agent_settings } from './_lib/agent_settings.js';
|
|
47
49
|
|
|
48
50
|
export const DEFAULT_FILE = 'agents/runtime/.agent-chat-history';
|
|
@@ -708,6 +710,26 @@ export function read_entries(
|
|
|
708
710
|
agent?: string | null | undefined;
|
|
709
711
|
} = {},
|
|
710
712
|
): Entry[] {
|
|
713
|
+
return read_entries_with_refs(options).map((p) => p.entry);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Like {@link read_entries} but each entry carries its `ref` — the 0-based
|
|
718
|
+
* ordinal in the file's FULL chronological entry list (header excluded,
|
|
719
|
+
* counted BEFORE any session/agent/last filter), so a ref from an index row
|
|
720
|
+
* stays valid regardless of the filters a later call applies. Refs are
|
|
721
|
+
* stable for the append-only log; file rotation invalidates them (documented
|
|
722
|
+
* — a ref is a within-file locator, not a durable id).
|
|
723
|
+
* (road-to-memory-retrieval-economy Phase 3.)
|
|
724
|
+
*/
|
|
725
|
+
export function read_entries_with_refs(
|
|
726
|
+
options: {
|
|
727
|
+
last?: number | null | undefined;
|
|
728
|
+
path?: string | null | undefined;
|
|
729
|
+
session?: string | null | undefined;
|
|
730
|
+
agent?: string | null | undefined;
|
|
731
|
+
} = {},
|
|
732
|
+
): Array<{ entry: Entry; ref: number }> {
|
|
711
733
|
const target = options.path ?? file_path();
|
|
712
734
|
const last = options.last ?? null;
|
|
713
735
|
const session = options.session ?? null;
|
|
@@ -715,8 +737,9 @@ export function read_entries(
|
|
|
715
737
|
if (!_isFile(target)) {
|
|
716
738
|
return [];
|
|
717
739
|
}
|
|
718
|
-
let
|
|
740
|
+
let pairs: Array<{ entry: Entry; ref: number }> = [];
|
|
719
741
|
const lines = _readLines(target);
|
|
742
|
+
let ref = 0;
|
|
720
743
|
for (let i = 0; i < lines.length; i++) {
|
|
721
744
|
const line = _strip(lines[i] ?? '');
|
|
722
745
|
if (!line) {
|
|
@@ -732,23 +755,72 @@ export function read_entries(
|
|
|
732
755
|
continue;
|
|
733
756
|
}
|
|
734
757
|
if (_isPlainObject(obj)) {
|
|
735
|
-
|
|
758
|
+
pairs.push({ entry: obj, ref });
|
|
759
|
+
ref += 1;
|
|
736
760
|
}
|
|
737
761
|
}
|
|
738
762
|
if (session !== null) {
|
|
739
|
-
|
|
763
|
+
pairs = pairs.filter((p) => p.entry.s === session);
|
|
740
764
|
}
|
|
741
765
|
if (agent !== null) {
|
|
742
766
|
if (agent === AGENT_UNKNOWN) {
|
|
743
|
-
|
|
767
|
+
pairs = pairs.filter((p) => !p.entry.agent);
|
|
744
768
|
} else {
|
|
745
|
-
|
|
769
|
+
pairs = pairs.filter((p) => p.entry.agent === agent);
|
|
746
770
|
}
|
|
747
771
|
}
|
|
748
772
|
if (last !== null && last >= 0) {
|
|
749
|
-
|
|
773
|
+
pairs = pairs.slice(pairs.length - last);
|
|
774
|
+
}
|
|
775
|
+
return pairs;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Timeline anchor: the entries around `ref` in the FULL chronological list —
|
|
780
|
+
* `before`/`after` neighbours plus the anchor itself. JSONL is chronological,
|
|
781
|
+
* so this is slicing, not indexing (road-to-memory-retrieval-economy P3).
|
|
782
|
+
* An out-of-range ref returns [].
|
|
783
|
+
*/
|
|
784
|
+
export function slice_around(
|
|
785
|
+
path_: string | null,
|
|
786
|
+
ref: number,
|
|
787
|
+
before = 3,
|
|
788
|
+
after = 3,
|
|
789
|
+
): Array<{ entry: Entry; ref: number }> {
|
|
790
|
+
const all = read_entries_with_refs({ path: path_ });
|
|
791
|
+
if (ref < 0 || ref >= all.length) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
const start = Math.max(0, ref - Math.max(0, before));
|
|
795
|
+
const end = Math.min(all.length, ref + Math.max(0, after) + 1);
|
|
796
|
+
return all.slice(start, end);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Compact index row for a history entry: ref + type tag + timestamp-ish
|
|
801
|
+
* fields when present + a ~100-char preview + the real-tokenizer cost of
|
|
802
|
+
* fetching the full entry (same lazy-tokenizer contract as memory_lookup's
|
|
803
|
+
* index rows).
|
|
804
|
+
*/
|
|
805
|
+
export function history_index_row(pair: { entry: Entry; ref: number }): Record<string, unknown> {
|
|
806
|
+
const e = pair.entry;
|
|
807
|
+
const text = typeof e.text === 'string' ? e.text : '';
|
|
808
|
+
const row: Record<string, unknown> = {
|
|
809
|
+
ref: pair.ref,
|
|
810
|
+
t: e.t ?? '',
|
|
811
|
+
preview: text.slice(0, 100),
|
|
812
|
+
tokens_estimate: _entry_tokens_estimate(e as Record<string, unknown>),
|
|
813
|
+
};
|
|
814
|
+
if (typeof e.s === 'string') {
|
|
815
|
+
row.s = e.s;
|
|
816
|
+
}
|
|
817
|
+
for (const tsField of ['ts', 'time', 'started']) {
|
|
818
|
+
if (typeof (e as Record<string, unknown>)[tsField] === 'string') {
|
|
819
|
+
row[tsField] = (e as Record<string, unknown>)[tsField];
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
750
822
|
}
|
|
751
|
-
return
|
|
823
|
+
return row;
|
|
752
824
|
}
|
|
753
825
|
|
|
754
826
|
export function read_entries_for_current(
|
|
@@ -108,6 +108,57 @@ export function evaluatePair(
|
|
|
108
108
|
return { id: task.id, winner, length_delta, winner_is_longer };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Cohen's Kappa — chance-corrected agreement between two raters over the same
|
|
113
|
+
* categorical labels (road-to-retrieval-substrate-hardening B7b). 1 = perfect,
|
|
114
|
+
* 0 = chance-level, <0 = worse than chance. `NaN` when there is no data.
|
|
115
|
+
*/
|
|
116
|
+
export function cohensKappa(labelsA: readonly string[], labelsB: readonly string[]): number {
|
|
117
|
+
const n = Math.min(labelsA.length, labelsB.length);
|
|
118
|
+
if (n === 0) return Number.NaN;
|
|
119
|
+
const cats = new Set<string>();
|
|
120
|
+
for (let i = 0; i < n; i++) {
|
|
121
|
+
cats.add(labelsA[i] as string);
|
|
122
|
+
cats.add(labelsB[i] as string);
|
|
123
|
+
}
|
|
124
|
+
let observedAgree = 0;
|
|
125
|
+
const countA = new Map<string, number>();
|
|
126
|
+
const countB = new Map<string, number>();
|
|
127
|
+
for (let i = 0; i < n; i++) {
|
|
128
|
+
const a = labelsA[i] as string;
|
|
129
|
+
const b = labelsB[i] as string;
|
|
130
|
+
if (a === b) observedAgree += 1;
|
|
131
|
+
countA.set(a, (countA.get(a) ?? 0) + 1);
|
|
132
|
+
countB.set(b, (countB.get(b) ?? 0) + 1);
|
|
133
|
+
}
|
|
134
|
+
const po = observedAgree / n;
|
|
135
|
+
let pe = 0;
|
|
136
|
+
for (const c of cats) pe += ((countA.get(c) ?? 0) / n) * ((countB.get(c) ?? 0) / n);
|
|
137
|
+
if (pe === 1) return 1; // both raters unanimous on one label → perfect by convention
|
|
138
|
+
return Math.round(((po - pe) / (1 - pe)) * 10000) / 10000;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Second-independent-judge validation: Cohen's Kappa over the two judges'
|
|
143
|
+
* per-pair winner labels (aligned by task id). A LOW kappa means the win-rate
|
|
144
|
+
* verdict rests on an unreliable grader — the trustworthiness gap the
|
|
145
|
+
* token-saving live run surfaced (33% judge-inconsistency), distinct from the
|
|
146
|
+
* effect-significance the Wilcoxon/McNemar gates already test.
|
|
147
|
+
*/
|
|
148
|
+
export function judgeKappa(resultsA: readonly PairResult[], resultsB: readonly PairResult[]): number {
|
|
149
|
+
const byIdB = new Map(resultsB.map((r) => [r.id, r.winner]));
|
|
150
|
+
const a: string[] = [];
|
|
151
|
+
const b: string[] = [];
|
|
152
|
+
for (const r of resultsA) {
|
|
153
|
+
const bw = byIdB.get(r.id);
|
|
154
|
+
if (bw !== undefined) {
|
|
155
|
+
a.push(r.winner);
|
|
156
|
+
b.push(bw);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return cohensKappa(a, b);
|
|
160
|
+
}
|
|
161
|
+
|
|
111
162
|
export interface QualityAggregate {
|
|
112
163
|
total: number;
|
|
113
164
|
thin_wins: number;
|
|
@@ -37,10 +37,11 @@ const ROADMAP_ROOT = 'agents/roadmaps';
|
|
|
37
37
|
// CHECKBOX_RE = re.compile(r"^\s*[-*]\s+\[([ xX~\-])\]\s", re.MULTILINE)
|
|
38
38
|
const CHECKBOX_RE = /^[ \t]*[-*][ \t]+\[([ xX~\-])\][ \t\f\v]/m;
|
|
39
39
|
// PHASE_RE = re.compile(
|
|
40
|
-
// r"^(#{2,3})\s+Phase\s+(\d+[a-z]?|[IVX]+|[A-Z](?:\d+)?)"
|
|
40
|
+
// r"^(#{2,3})\s+Phase\s+(\d+(?:\.\d+)*[a-z]?|[IVX]+|[A-Z](?:\d+)?)"
|
|
41
41
|
// r"(?:[\s:—\-]+(.*?))?\s*$", re.MULTILINE)
|
|
42
|
+
// Numeric ids accept dotted sub-phases ("1.0", "4.1") — mirrors the dashboard's PHASE_RE.
|
|
42
43
|
const PHASE_RE =
|
|
43
|
-
/^(#{2,3})[ \t\f\v]+Phase[ \t\f\v]+(\d+[a-z]?|[IVX]+|[A-Z](?:\d+)?)(?:[\s:—\-]+(.*?))?[ \t\f\v]*$/gm;
|
|
44
|
+
/^(#{2,3})[ \t\f\v]+Phase[ \t\f\v]+(\d+(?:\.\d+)*[a-z]?|[IVX]+|[A-Z](?:\d+)?)(?:[\s:—\-]+(.*?))?[ \t\f\v]*$/gm;
|
|
44
45
|
// FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\s*\n", re.DOTALL)
|
|
45
46
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\s*\n/;
|
|
46
47
|
const DRAFT_VALUES: ReadonlySet<string> = new Set(['draft']);
|
package/src/scripts/condense.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
strip_source_prefix,
|
|
41
41
|
} from './_lib/agent_src.js';
|
|
42
42
|
import { build_claude_hook_matrix } from './_lib/claude_settings_hooks.js';
|
|
43
|
+
import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
|
|
43
44
|
import { project_settings_path, load_agent_settings } from './_lib/agent_settings.js';
|
|
44
45
|
import { info, success, flush_summary, resolve_level } from './_lib/script_output.js';
|
|
45
46
|
import {
|
|
@@ -1529,6 +1530,7 @@ export function generate_claude_commands(active_command_slugs: ReadonlySet<strin
|
|
|
1529
1530
|
const current_slugs = new Set<string>();
|
|
1530
1531
|
let count = 0;
|
|
1531
1532
|
let skipped = 0;
|
|
1533
|
+
let reserved = 0;
|
|
1532
1534
|
let rendered = 0;
|
|
1533
1535
|
const auto = _read_model_auto_switch() === 'auto';
|
|
1534
1536
|
for (const [source_file, slug] of iterCommands()) {
|
|
@@ -1539,6 +1541,15 @@ export function generate_claude_commands(active_command_slugs: ReadonlySet<strin
|
|
|
1539
1541
|
skipped += 1;
|
|
1540
1542
|
continue;
|
|
1541
1543
|
}
|
|
1544
|
+
// Never claim a `/name` that is a Claude Code built-in command or
|
|
1545
|
+
// bundled skill — a same-name entry shadows the built-in (e.g. a
|
|
1546
|
+
// `review` entry hides Claude Code's own /review). Nested
|
|
1547
|
+
// `/cluster:sub` slugs are hyphen-joined and cannot collide.
|
|
1548
|
+
// See src/scripts/_lib/claude_builtin_names.ts.
|
|
1549
|
+
if (is_claude_builtin_name(slug)) {
|
|
1550
|
+
reserved += 1;
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1542
1553
|
current_slugs.add(slug);
|
|
1543
1554
|
|
|
1544
1555
|
const skill_dir = path.join(MODULE_STATE.CLAUDE_SKILLS_DIR, slug);
|
|
@@ -1588,6 +1599,9 @@ export function generate_claude_commands(active_command_slugs: ReadonlySet<strin
|
|
|
1588
1599
|
if (skipped) {
|
|
1589
1600
|
msg += ` (${skipped} skipped — same-name skill exists)`;
|
|
1590
1601
|
}
|
|
1602
|
+
if (reserved) {
|
|
1603
|
+
msg += ` (${reserved} withheld — Claude Code built-in name)`;
|
|
1604
|
+
}
|
|
1591
1605
|
if (removed_dirs) {
|
|
1592
1606
|
msg += ` (${removed_dirs} stale dirs removed)`;
|
|
1593
1607
|
}
|
|
Binary file
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Domain-soundness validation status + ratchet
|
|
4
|
+
* (road-to-domain-soundness Phase 1 + Phase 3).
|
|
5
|
+
*
|
|
6
|
+
* The four non-coding profiles (finance, founder, ops, content_creator) SELL
|
|
7
|
+
* concrete domain value (DCF, runway, RICE, incident command, messaging), but
|
|
8
|
+
* the skills are forged on TS/PHP — "promising, not proven" off those stacks.
|
|
9
|
+
* A disclaimer floor bounds liability, NOT correctness. This tool makes the
|
|
10
|
+
* provenance honest and machine-checkable: it enumerates the default-surface
|
|
11
|
+
* domain skills those profiles ship (from source — each profile's
|
|
12
|
+
* `skills_hint`, never hardcoded) and reports each as:
|
|
13
|
+
*
|
|
14
|
+
* - `validated` — ships `evals/domain-truth.json` (a sourced answer key,
|
|
15
|
+
* authored by domain competence, distinct from behavioural
|
|
16
|
+
* evals.json). Presence is the machine signal; a passing
|
|
17
|
+
* run against it is the maintainer's Phase-3 gate.
|
|
18
|
+
* - `unvalidated` — no domain-truth fixture: general-purpose scaffold, domain
|
|
19
|
+
* correctness not independently validated.
|
|
20
|
+
*
|
|
21
|
+
* A domain skill can pass a format/behavioural eval AND carry a disclaimer AND
|
|
22
|
+
* still embed a wrong domain assumption — this axis is the one that catches it.
|
|
23
|
+
*
|
|
24
|
+
* Modes:
|
|
25
|
+
* (default) human report.
|
|
26
|
+
* --json machine-readable.
|
|
27
|
+
* --check ratchet gate (CI): the count of `validated` skills may not
|
|
28
|
+
* drop below the pinned floor (internal/evals/domain-soundness-floor.json);
|
|
29
|
+
* a missing floor is an all-zero floor (bootstrap-inert). The
|
|
30
|
+
* floor only rises via --write-floor after a fixture lands and
|
|
31
|
+
* passes. This is the enforced-honesty gate: it makes a claimed
|
|
32
|
+
* validation impossible to assert without the fixture on disk.
|
|
33
|
+
* --write-floor pin the current validated count as the floor (maintainer
|
|
34
|
+
* action after authoring + running a domain-truth fixture).
|
|
35
|
+
*
|
|
36
|
+
* Exit codes: 0 ok / 1 ratchet regression / 2 usage 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
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
45
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
46
|
+
|
|
47
|
+
/** The non-coding profiles whose default surface this tool scopes. */
|
|
48
|
+
export const NON_CODING_PROFILES: readonly string[] = [
|
|
49
|
+
'finance',
|
|
50
|
+
'founder',
|
|
51
|
+
'ops',
|
|
52
|
+
'content_creator',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export interface StatusOptions {
|
|
56
|
+
skillsDir?: string;
|
|
57
|
+
profilesDir?: string;
|
|
58
|
+
floorPath?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SkillStatus {
|
|
62
|
+
skill: string;
|
|
63
|
+
status: 'validated' | 'unvalidated';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SoundnessReport {
|
|
67
|
+
validationSet: SkillStatus[];
|
|
68
|
+
validated: number;
|
|
69
|
+
unvalidated: number;
|
|
70
|
+
total: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _skillsDir(o: StatusOptions): string {
|
|
74
|
+
return o.skillsDir ?? path.join(REPO_ROOT, 'src', 'skills');
|
|
75
|
+
}
|
|
76
|
+
function _profilesDir(o: StatusOptions): string {
|
|
77
|
+
return o.profilesDir ?? path.join(REPO_ROOT, 'src', 'agent-src', 'profiles');
|
|
78
|
+
}
|
|
79
|
+
export function _floorPath(o: StatusOptions = {}): string {
|
|
80
|
+
return (
|
|
81
|
+
o.floorPath ??
|
|
82
|
+
path.join(REPO_ROOT, 'internal', 'evals', 'domain-soundness-floor.json')
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Union of the non-coding profiles' `skills_hint`, restricted to real skills. */
|
|
87
|
+
export function validationSet(o: StatusOptions = {}): string[] {
|
|
88
|
+
const profilesDir = _profilesDir(o);
|
|
89
|
+
const skillsDir = _skillsDir(o);
|
|
90
|
+
const out = new Set<string>();
|
|
91
|
+
for (const p of NON_CODING_PROFILES) {
|
|
92
|
+
const f = path.join(profilesDir, `${p}.yml`);
|
|
93
|
+
let doc: unknown;
|
|
94
|
+
try {
|
|
95
|
+
doc = parseYaml(fs.readFileSync(f, 'utf-8'));
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const profile = (doc as { profile?: unknown } | null)?.profile;
|
|
100
|
+
const defaults = (profile as { defaults?: unknown } | null)?.defaults;
|
|
101
|
+
const hint = (defaults as { skills_hint?: unknown } | null)?.skills_hint;
|
|
102
|
+
if (Array.isArray(hint)) {
|
|
103
|
+
for (const s of hint) {
|
|
104
|
+
if (typeof s === 'string' && fs.existsSync(path.join(skillsDir, s, 'SKILL.md'))) {
|
|
105
|
+
out.add(s);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...out].sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _isValidated(skillsDir: string, skill: string): boolean {
|
|
114
|
+
return fs.existsSync(path.join(skillsDir, skill, 'evals', 'domain-truth.json'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function computeStatus(o: StatusOptions = {}): SoundnessReport {
|
|
118
|
+
const skillsDir = _skillsDir(o);
|
|
119
|
+
const set = validationSet(o);
|
|
120
|
+
const validationSetStatus: SkillStatus[] = set.map((skill) => ({
|
|
121
|
+
skill,
|
|
122
|
+
status: _isValidated(skillsDir, skill) ? 'validated' : 'unvalidated',
|
|
123
|
+
}));
|
|
124
|
+
const validated = validationSetStatus.filter((s) => s.status === 'validated').length;
|
|
125
|
+
return {
|
|
126
|
+
validationSet: validationSetStatus,
|
|
127
|
+
validated,
|
|
128
|
+
unvalidated: validationSetStatus.length - validated,
|
|
129
|
+
total: validationSetStatus.length,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _readFloor(floorPath: string): number {
|
|
134
|
+
try {
|
|
135
|
+
const raw = JSON.parse(fs.readFileSync(floorPath, 'utf-8')) as { validated?: number };
|
|
136
|
+
return raw.validated ?? 0;
|
|
137
|
+
} catch {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function writeFloor(o: StatusOptions = {}): SoundnessReport {
|
|
143
|
+
const report = computeStatus(o);
|
|
144
|
+
const p = _floorPath(o);
|
|
145
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
p,
|
|
148
|
+
JSON.stringify(
|
|
149
|
+
{
|
|
150
|
+
_comment:
|
|
151
|
+
'Domain-soundness ratchet floor (road-to-domain-soundness Phase 3). ' +
|
|
152
|
+
'domain_soundness_status --check fails if the validated count drops below this. ' +
|
|
153
|
+
'Regenerate with --write-floor ONLY after a domain-truth fixture lands and passes.',
|
|
154
|
+
validated: report.validated,
|
|
155
|
+
},
|
|
156
|
+
null,
|
|
157
|
+
2,
|
|
158
|
+
) + '\n',
|
|
159
|
+
);
|
|
160
|
+
return report;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function checkRatchet(o: StatusOptions = {}): { ok: boolean; message: string } {
|
|
164
|
+
const report = computeStatus(o);
|
|
165
|
+
const floor = _readFloor(_floorPath(o));
|
|
166
|
+
if (report.validated < floor) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
message: `validated domain skills ${report.validated} < floor ${floor}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { ok: true, message: `validated ${report.validated}/${report.total} (floor ${floor})` };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _renderHuman(r: SoundnessReport): string {
|
|
176
|
+
const lines = [
|
|
177
|
+
'Domain-soundness status — non-coding default-surface skills',
|
|
178
|
+
` validated: ${r.validated}/${r.total}`,
|
|
179
|
+
` unvalidated: ${r.unvalidated}/${r.total}`,
|
|
180
|
+
'',
|
|
181
|
+
];
|
|
182
|
+
for (const s of r.validationSet) {
|
|
183
|
+
const mark = s.status === 'validated' ? '✅' : '·';
|
|
184
|
+
lines.push(` ${mark} ${s.skill.padEnd(28)} ${s.status}`);
|
|
185
|
+
}
|
|
186
|
+
return lines.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
190
|
+
const known = new Set(['--json', '--check', '--write-floor']);
|
|
191
|
+
for (const a of argv) {
|
|
192
|
+
if (!known.has(a)) {
|
|
193
|
+
process.stderr.write('usage: domain_soundness_status [--json | --check | --write-floor]\n');
|
|
194
|
+
return 2;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (argv.includes('--write-floor')) {
|
|
198
|
+
const r = writeFloor();
|
|
199
|
+
process.stdout.write(`✅ domain-soundness floor pinned: validated ${r.validated}/${r.total}\n`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
if (argv.includes('--check')) {
|
|
203
|
+
const { ok, message } = checkRatchet();
|
|
204
|
+
if (!ok) {
|
|
205
|
+
process.stdout.write(`❌ domain-soundness ratchet regressed: ${message}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
process.stdout.write(`✅ domain-soundness ratchet: ${message}.\n`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
const report = computeStatus();
|
|
212
|
+
if (argv.includes('--json')) {
|
|
213
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
process.stdout.write(_renderHuman(report) + '\n');
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _isCliEntry(): boolean {
|
|
221
|
+
if (process.argv[1] === undefined) return false;
|
|
222
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
223
|
+
if (import.meta.url === argvUrl) return true;
|
|
224
|
+
try {
|
|
225
|
+
return (
|
|
226
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
227
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (_isCliEntry()) {
|
|
235
|
+
process.exit(main());
|
|
236
|
+
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
iter_commands,
|
|
25
25
|
strip_source_prefix,
|
|
26
26
|
} from './_lib/agent_src.js';
|
|
27
|
+
import { computeStatus as domainSoundnessStatus } from './domain_soundness_status.js';
|
|
27
28
|
|
|
28
29
|
const _HERE = fileURLToPath(import.meta.url);
|
|
29
30
|
|
|
@@ -398,6 +399,7 @@ export function _render_catalog(
|
|
|
398
399
|
const publicRules = rules.filter((r) => !INTERNAL_RULES.has(r.name));
|
|
399
400
|
const publicCommands = commands.filter((c) => c.kind === 'command');
|
|
400
401
|
const total = skills.length + publicRules.length + publicCommands.length + guidelines.length;
|
|
402
|
+
const ds = domainSoundnessStatus();
|
|
401
403
|
const parts = [
|
|
402
404
|
'# agent-config — Public Catalog',
|
|
403
405
|
'',
|
|
@@ -410,6 +412,21 @@ export function _render_catalog(
|
|
|
410
412
|
'',
|
|
411
413
|
`## Skills (${skills.length})`,
|
|
412
414
|
'',
|
|
415
|
+
'> **Behavioural-eval coverage is measured, not implied.** A skill is',
|
|
416
|
+
'> behaviourally evaluated only if it ships `evals/evals.json`; most do',
|
|
417
|
+
'> not yet. Coverage is reported per tier (`./scripts-run',
|
|
418
|
+
'> src/scripts/skill_eval_coverage`) and CI-ratcheted so it can only rise.',
|
|
419
|
+
'> Listing here never implies a skill is behaviourally tested — the',
|
|
420
|
+
'> current coverage and its gap are published on [the proof page](proof.md).',
|
|
421
|
+
'',
|
|
422
|
+
'> **Non-coding domain correctness is scoped, not proven.** The',
|
|
423
|
+
'> `finance` / `founder` / `ops` / `content` skills are forged on TS/PHP',
|
|
424
|
+
`> codebases; ${ds.validated} of ${ds.total} default-surface domain skills carry a sourced`,
|
|
425
|
+
'> `evals/domain-truth.json` fixture (deterministic targets, keys from',
|
|
426
|
+
'> cited formulas); the rest are labeled `unvalidated` until they pass one',
|
|
427
|
+
'> (`./scripts-run src/scripts/domain_soundness_status`). A disclaimer',
|
|
428
|
+
'> floor bounds liability, not correctness — see [the proof page](proof.md).',
|
|
429
|
+
'',
|
|
413
430
|
_render_table(skills, ['kind', 'name', 'extra', 'description'], '../', _to_shipped_path),
|
|
414
431
|
'',
|
|
415
432
|
`## Rules (${publicRules.length})`,
|