@event4u/agent-config 8.8.0 → 8.10.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 +139 -263
- package/README.md +5 -5
- package/dist/agent-src/commands/agent-handoff.md +39 -2
- package/dist/agent-src/commands/cost/report.md +13 -3
- 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/commands/tests/create.md +28 -6
- package/dist/agent-src/commands/work.md +33 -0
- package/dist/agent-src/commands/worktree/cleanup.md +78 -0
- package/dist/agent-src/commands/worktree/create.md +62 -0
- package/dist/agent-src/commands/worktree/status.md +63 -0
- package/dist/agent-src/commands/worktree/verify.md +70 -0
- package/dist/agent-src/commands/worktree.md +85 -0
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +60 -31
- 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/agent-authority.md +1 -1
- package/dist/agent-src/rules/communication-through-line.md +1 -1
- package/dist/agent-src/rules/delegation-policy.md +5 -0
- package/dist/agent-src/rules/design-fidelity.md +40 -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 +58 -10
- package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
- package/dist/agent-src/rules/question-not-instruction.md +76 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +1 -1
- package/dist/agent-src/rules/spreadsheet-source-quality.md +61 -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/accessibility-auditor/evals/evals.json +15 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +10 -3
- package/dist/agent-src/skills/analysis-skill-router/evals/evals.json +13 -0
- package/dist/agent-src/skills/api-testing/SKILL.md +15 -1
- package/dist/agent-src/skills/character-consistency/evals/evals.json +16 -0
- package/dist/agent-src/skills/code-review/SKILL.md +2 -0
- package/dist/agent-src/skills/command-routing/evals/evals.json +14 -0
- package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
- package/dist/agent-src/skills/dashboard-design/evals/evals.json +16 -0
- package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/dcf-modeling/evals/evals.json +20 -0
- package/dist/agent-src/skills/decision-record/evals/evals.json +20 -0
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
- package/dist/agent-src/skills/design-intelligence/evals/evals.json +14 -0
- package/dist/agent-src/skills/design-review/SKILL.md +35 -15
- package/dist/agent-src/skills/design-system-capture/SKILL.md +84 -24
- package/dist/agent-src/skills/design-system-capture/evals/evals.json +15 -0
- package/dist/agent-src/skills/developer-like-execution/evals/evals.json +16 -0
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +27 -0
- package/dist/agent-src/skills/doc-coauthoring/evals/evals.json +16 -0
- package/dist/agent-src/skills/editorial-calendar/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/editorial-calendar/evals/evals.json +16 -0
- package/dist/agent-src/skills/estimate-ticket/evals/evals.json +16 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -0
- package/dist/agent-src/skills/existing-ui-audit/evals/triggers.json +47 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +36 -5
- package/dist/agent-src/skills/fe-design/evals/triggers.json +47 -0
- package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/forecasting/evals/evals.json +20 -0
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +2 -2
- package/dist/agent-src/skills/fundraising-narrative/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/fundraising-narrative/evals/evals.json +16 -0
- package/dist/agent-src/skills/html-deck/SKILL.md +27 -0
- package/dist/agent-src/skills/iconography/SKILL.md +24 -0
- package/dist/agent-src/skills/incident-commander/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/incident-commander/evals/evals.json +20 -0
- package/dist/agent-src/skills/launch-readiness/evals/evals.json +21 -0
- package/dist/agent-src/skills/logging-monitoring/evals/evals.json +16 -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/messaging-architecture/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/messaging-architecture/evals/evals.json +16 -0
- package/dist/agent-src/skills/perf-feedback-craft/evals/evals.json +16 -0
- package/dist/agent-src/skills/pest-testing/SKILL.md +1 -1
- package/dist/agent-src/skills/playwright-testing/SKILL.md +16 -0
- package/dist/agent-src/skills/prompt-engineering-patterns/evals/evals.json +16 -0
- package/dist/agent-src/skills/refine-prompt/evals/evals.json +16 -0
- package/dist/agent-src/skills/refine-ticket/evals/evals.json +16 -0
- package/dist/agent-src/skills/release-comms/evals/evals.json +16 -0
- package/dist/agent-src/skills/rice-prioritization/evals/evals.json +20 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +39 -5
- package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/runway-cognition/evals/evals.json +20 -0
- package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/scenario-modeling/evals/evals.json +20 -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/spreadsheet-authoring/SKILL.md +98 -0
- package/dist/agent-src/skills/spreadsheet-authoring/evals/triggers.json +47 -0
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +12 -0
- package/dist/agent-src/skills/systematic-debugging/evals/evals.json +20 -0
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +118 -0
- package/dist/agent-src/skills/test-case-discovery/evals/triggers.json +52 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +32 -6
- package/dist/agent-src/skills/test-driven-development/evals/evals.json +16 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +34 -4
- package/dist/agent-src/skills/threat-modeling/evals/evals.json +21 -0
- package/dist/agent-src/skills/typography-system/evals/evals.json +14 -0
- 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/skills/unit-economics-modeling/evals/evals.json +24 -0
- package/dist/agent-src/skills/verify-completion-evidence/evals/evals.json +16 -0
- package/dist/agent-src/skills/vision-articulation/evals/evals.json +16 -0
- package/dist/agent-src/skills/voice-and-tone-design/evals/evals.json +16 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +180 -0
- package/dist/agent-src/skills/worktree-lifecycle/evals/triggers.json +52 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +36 -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 +326 -67
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +25 -14
- package/dist/discovery/trust-report.md +4 -4
- package/dist/discovery/workspaces.json +25 -12
- package/dist/install/install.mjs +136 -1
- package/dist/mcp/registry-manifest.json +3 -3
- 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/docs/CLAIMS.md +36 -8
- package/docs/architecture.md +4 -4
- package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
- package/docs/benchmark.md +113 -0
- package/docs/catalog.md +32 -8
- package/docs/command-flows.md +6 -1
- package/docs/contracts/CHANGELOG-conventions.md +6 -0
- package/docs/contracts/command-clusters.md +18 -0
- package/docs/contracts/design-artifact-lifecycle.md +93 -0
- package/docs/contracts/design-artifact-verification.md +153 -0
- package/docs/contracts/injected-block-authenticity-model.md +66 -0
- package/docs/contracts/mcp-tool-inventory.md +28 -28
- package/docs/contracts/release-pr-gating.md +26 -0
- package/docs/contracts/release-sizing.md +85 -0
- package/docs/contracts/surface-agent-contracts.md +153 -0
- package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
- package/docs/decisions/ADR-115-command-cluster-consolidation-phase-4.md +1 -1
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +9 -0
- package/docs/distribution/consumer-matrix.md +96 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +2 -2
- 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/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 +38 -23
- package/docs/release-runbook.md +3 -0
- package/docs/second-brain-scope.md +69 -26
- package/docs/self-review-gate.md +51 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
- 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_record.ts +183 -0
- package/src/scripts/_lib/orchestration_savings.ts +114 -9
- 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 +30 -17
- package/src/scripts/check_memory.ts +2 -0
- package/src/scripts/check_quality_regression.ts +51 -0
- package/src/scripts/check_release_adjacent_health.ts +121 -0
- package/src/scripts/check_roadmap_trackable.ts +3 -2
- package/src/scripts/check_rule_invariants.ts +275 -0
- package/src/scripts/condense.ts +14 -0
- package/src/scripts/consumer_matrix.ts +353 -0
- package/src/scripts/discovery_graph.ts +0 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_index.ts +6 -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_changelog_rollback.ts +293 -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 +2 -1
- package/src/scripts/mcp_server/tools.ts +24 -1
- package/src/scripts/measure_lexical_ranking.ts +173 -0
- package/src/scripts/memory_lookup.ts +0 -0
- package/src/scripts/orchestration_record.ts +156 -0
- package/src/scripts/orchestration_savings_report.ts +65 -8
- 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 +320 -0
- package/src/scripts/second_brain_run.ts +341 -0
- package/src/scripts/self_review_gate.test.ts +65 -0
- package/src/scripts/self_review_gate.ts +272 -0
- package/src/scripts/trigger_eval_grandfather.json +223 -224
|
@@ -477,6 +477,18 @@ async function _memoryLookupHandler(
|
|
|
477
477
|
throw new Error("'detail' must be 'index' or 'full'");
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
// B1 (road-to-retrieval-substrate-hardening): optional token budget →
|
|
481
|
+
// compact one-line rows, hard-cut with a truncation hint. Absent → the
|
|
482
|
+
// envelope is unchanged (v1 default-full contract holds).
|
|
483
|
+
const opts: { detail: 'full' | 'index'; token_budget?: number } = { detail: detailRaw };
|
|
484
|
+
if (args.token_budget !== undefined) {
|
|
485
|
+
const tb = args.token_budget;
|
|
486
|
+
if (typeof tb !== 'number' || !Number.isInteger(tb) || typeof tb === 'boolean' || tb < 1) {
|
|
487
|
+
throw new Error("'token_budget' must be a positive integer");
|
|
488
|
+
}
|
|
489
|
+
opts.token_budget = tb;
|
|
490
|
+
}
|
|
491
|
+
|
|
480
492
|
const prevCwd = process.cwd();
|
|
481
493
|
let envelope: Record<string, unknown>;
|
|
482
494
|
try {
|
|
@@ -485,7 +497,7 @@ async function _memoryLookupHandler(
|
|
|
485
497
|
[...(types as string[])],
|
|
486
498
|
[...(keys as string[])],
|
|
487
499
|
limitRaw,
|
|
488
|
-
|
|
500
|
+
opts,
|
|
489
501
|
);
|
|
490
502
|
} finally {
|
|
491
503
|
process.chdir(prevCwd);
|
|
@@ -1400,6 +1412,17 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
|
1400
1412
|
"this first, then memory_get the ids you need. " +
|
|
1401
1413
|
"'full' (default) returns complete entries.",
|
|
1402
1414
|
},
|
|
1415
|
+
token_budget: {
|
|
1416
|
+
type: 'integer',
|
|
1417
|
+
minimum: 1,
|
|
1418
|
+
description:
|
|
1419
|
+
'Optional token budget. When set, entries are ' +
|
|
1420
|
+
'rendered as one-line compact rows (id, type, ' +
|
|
1421
|
+
'confidence, `line`) and the row set is hard-cut at ' +
|
|
1422
|
+
'token_budget × 4 chars; omitted hits appear as a ' +
|
|
1423
|
+
'top-level `truncation` hint naming a concrete next ' +
|
|
1424
|
+
'step. Absent → the envelope is unchanged.',
|
|
1425
|
+
},
|
|
1403
1426
|
},
|
|
1404
1427
|
required: ['types'],
|
|
1405
1428
|
additionalProperties: false,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Ranking-lift measurement for the hand-rolled lexical index
|
|
4
|
+
* (road-to-retrieval-substrate-hardening B2, deterministic + free).
|
|
5
|
+
*
|
|
6
|
+
* The gate on shipping the index (roadmap Phase 2): it must measurably rank
|
|
7
|
+
* BETTER than the current `_score` bucket scorer, else honest-null. This
|
|
8
|
+
* script runs both scorers over the SAME retrieval-precision corpus + store
|
|
9
|
+
* (internal/bench/second-brain/retrieval-store/) and reports, per task and in
|
|
10
|
+
* aggregate:
|
|
11
|
+
*
|
|
12
|
+
* - mean top tie-set size — how many entries share the TOP score. `_score`
|
|
13
|
+
* gives 0.8 to any keyword match, so the needed entry ties with its
|
|
14
|
+
* confusers (the "recalls but does not rank" finding, mean 3.3). A good
|
|
15
|
+
* ranker drives this toward 1.
|
|
16
|
+
* - precision@1 / @k — was the NEEDED entry actually first / in the top-k.
|
|
17
|
+
*
|
|
18
|
+
* No model, no spend: pure retrieval scoring. Exit 0 ok / 1 error / 2 usage.
|
|
19
|
+
*
|
|
20
|
+
* Usage: measure_lexical_ranking.ts [--write] [--format text|json]
|
|
21
|
+
* --write persist the report to internal/bench/reports/lexical-ranking.json
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
import * as YAML from 'yaml';
|
|
28
|
+
|
|
29
|
+
import { LexicalIndex } from './_lib/lexical_index.js';
|
|
30
|
+
import { _score, _setIntakeRoot, _setKnowledgeRoot, _setMemoryRoot, retrieve } from './memory_lookup.js';
|
|
31
|
+
|
|
32
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
33
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
34
|
+
export const STORE_DIR = path.join(REPO_ROOT, 'internal', 'bench', 'second-brain', 'retrieval-store');
|
|
35
|
+
export const CORPUS = path.join(STORE_DIR, 'retrieval-corpus.yml');
|
|
36
|
+
export const REPORT = path.join(REPO_ROOT, 'internal', 'bench', 'reports', 'lexical-ranking.json');
|
|
37
|
+
|
|
38
|
+
interface Task {
|
|
39
|
+
id: string;
|
|
40
|
+
query: string[];
|
|
41
|
+
needed: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TaskResult {
|
|
45
|
+
id: string;
|
|
46
|
+
needed: string;
|
|
47
|
+
baseline: { rank: number; tieSet: number };
|
|
48
|
+
index: { rank: number; tieSet: number };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface RankingSummary {
|
|
52
|
+
tasks: number;
|
|
53
|
+
k: number;
|
|
54
|
+
baseline_mean_tie_set: number;
|
|
55
|
+
index_mean_tie_set: number;
|
|
56
|
+
baseline_precision_at_1: number;
|
|
57
|
+
index_precision_at_1: number;
|
|
58
|
+
baseline_precision_at_k: number;
|
|
59
|
+
index_precision_at_k: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Text an entry contributes to the index — the fields `_score` also reads. */
|
|
63
|
+
function entryText(entry: Record<string, unknown>): string {
|
|
64
|
+
const parts: string[] = [];
|
|
65
|
+
for (const f of ['path', 'key', 'symptom', 'feature', 'rule', 'body']) {
|
|
66
|
+
const v = entry[f];
|
|
67
|
+
if (typeof v === 'string') parts.push(v);
|
|
68
|
+
else if (Array.isArray(v)) for (const x of v) parts.push(String(x));
|
|
69
|
+
}
|
|
70
|
+
return parts.join(' | ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Top tie-set size + rank of the needed id in a sorted (id, score) list. */
|
|
74
|
+
function diagnose(ranked: Array<{ id: string; score: number }>, needed: string): { rank: number; tieSet: number } {
|
|
75
|
+
if (ranked.length === 0) return { rank: -1, tieSet: 0 };
|
|
76
|
+
const top = ranked[0]?.score ?? 0;
|
|
77
|
+
const tieSet = ranked.filter((r) => r.score === top).length;
|
|
78
|
+
const rank = ranked.findIndex((r) => r.id === needed);
|
|
79
|
+
return { rank, tieSet };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function measure(): { tasks: TaskResult[]; summary: RankingSummary } {
|
|
83
|
+
_setMemoryRoot(STORE_DIR);
|
|
84
|
+
_setKnowledgeRoot(path.join(STORE_DIR, 'knowledge-none'));
|
|
85
|
+
_setIntakeRoot(path.join(STORE_DIR, 'intake-none'));
|
|
86
|
+
|
|
87
|
+
const doc = YAML.parse(fs.readFileSync(CORPUS, 'utf-8')) as { type: string; k: number; tasks: Task[] };
|
|
88
|
+
const type = doc.type;
|
|
89
|
+
const k = doc.k;
|
|
90
|
+
const results: TaskResult[] = [];
|
|
91
|
+
|
|
92
|
+
for (const task of doc.tasks) {
|
|
93
|
+
// Shared universe: every store entry `_score` considers relevant.
|
|
94
|
+
const hits = retrieve([type], task.query, 100);
|
|
95
|
+
|
|
96
|
+
// Baseline — rank by the current bucket scorer, ties broken by the
|
|
97
|
+
// order `retrieve` already returns (store order), exactly as today.
|
|
98
|
+
const baselineRanked = hits.map((h) => ({ id: h.id, score: h.score }));
|
|
99
|
+
const baseline = diagnose(baselineRanked, task.needed);
|
|
100
|
+
|
|
101
|
+
// Index — BM25 over the same entries' text.
|
|
102
|
+
const index = new LexicalIndex(hits.map((h) => ({ id: h.id, text: entryText(h.entry) })));
|
|
103
|
+
const indexRanked = index.rank(task.query);
|
|
104
|
+
const idx = diagnose(indexRanked, task.needed);
|
|
105
|
+
|
|
106
|
+
results.push({ id: task.id, needed: task.needed, baseline, index: idx });
|
|
107
|
+
void _score; // referenced for provenance — retrieve() applies it internally
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const n = results.length;
|
|
111
|
+
const mean = (sel: (r: TaskResult) => number): number =>
|
|
112
|
+
n === 0 ? 0 : Math.round((results.reduce((a, r) => a + sel(r), 0) / n) * 1000) / 1000;
|
|
113
|
+
const precAt = (sel: (r: TaskResult) => number, kk: number): number =>
|
|
114
|
+
n === 0 ? 0 : Math.round((results.filter((r) => sel(r) >= 0 && sel(r) < kk).length / n) * 1000) / 1000;
|
|
115
|
+
|
|
116
|
+
const summary = {
|
|
117
|
+
tasks: n,
|
|
118
|
+
k,
|
|
119
|
+
baseline_mean_tie_set: mean((r) => r.baseline.tieSet),
|
|
120
|
+
index_mean_tie_set: mean((r) => r.index.tieSet),
|
|
121
|
+
baseline_precision_at_1: precAt((r) => r.baseline.rank, 1),
|
|
122
|
+
index_precision_at_1: precAt((r) => r.index.rank, 1),
|
|
123
|
+
baseline_precision_at_k: precAt((r) => r.baseline.rank, k),
|
|
124
|
+
index_precision_at_k: precAt((r) => r.index.rank, k),
|
|
125
|
+
};
|
|
126
|
+
return { tasks: results, summary };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function main(argv: string[]): number {
|
|
130
|
+
let write = false;
|
|
131
|
+
let format: 'text' | 'json' = 'text';
|
|
132
|
+
for (let i = 0; i < argv.length; i++) {
|
|
133
|
+
const a = argv[i] as string;
|
|
134
|
+
if (a === '--write') write = true;
|
|
135
|
+
else if (a === '--format') format = (argv[++i] as 'text' | 'json') ?? 'text';
|
|
136
|
+
else if (a === '-h' || a === '--help') {
|
|
137
|
+
process.stdout.write('usage: measure_lexical_ranking.ts [--write] [--format text|json]\n');
|
|
138
|
+
return 0;
|
|
139
|
+
} else {
|
|
140
|
+
process.stderr.write(`measure_lexical_ranking: unknown argument ${a}\n`);
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let out: { tasks: TaskResult[]; summary: RankingSummary };
|
|
146
|
+
try {
|
|
147
|
+
out = measure();
|
|
148
|
+
} catch (exc) {
|
|
149
|
+
process.stderr.write(`measure_lexical_ranking: error: ${String(exc)}\n`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (write) {
|
|
154
|
+
fs.mkdirSync(path.dirname(REPORT), { recursive: true });
|
|
155
|
+
fs.writeFileSync(REPORT, JSON.stringify({ schema_version: 1, ...out }, null, 2) + '\n');
|
|
156
|
+
}
|
|
157
|
+
if (format === 'json') {
|
|
158
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
const s = out.summary;
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
`lexical-ranking (${s.tasks} tasks, k=${s.k}):\n` +
|
|
164
|
+
` mean top tie-set baseline=${s.baseline_mean_tie_set} index=${s.index_mean_tie_set}\n` +
|
|
165
|
+
` precision@1 baseline=${s.baseline_precision_at_1} index=${s.index_precision_at_1}\n` +
|
|
166
|
+
` precision@${s.k} baseline=${s.baseline_precision_at_k} index=${s.index_precision_at_k}\n`,
|
|
167
|
+
);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
|
|
172
|
+
process.exit(main(process.argv.slice(2)));
|
|
173
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration telemetry recorder — CLI.
|
|
3
|
+
*
|
|
4
|
+
* Appends ONE validated audit-log-v1 line (with an `orchestration` object) to
|
|
5
|
+
* the monthly audit log, from the counts the orchestrating agent has after an
|
|
6
|
+
* auto-dispatch. One command instead of hand-authored JSON — the reliable,
|
|
7
|
+
* lock-respecting (2026-06-30 no-hook decision) capture path.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ./scripts-run src/scripts/orchestration_record \
|
|
11
|
+
* --spawn-count 1 --token-delta -72000 --provenance measured \
|
|
12
|
+
* --tier-chosen lite --tier-source inferred --task-class read-only-fanout \
|
|
13
|
+
* [--tiers sonnet,opus] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
|
|
14
|
+
* [--first-pass-success true|false] [--escalated true|false] \
|
|
15
|
+
* [--dir <audit-dir>] [--dry-run]
|
|
16
|
+
*
|
|
17
|
+
* Read by `src/scripts/orchestration_savings_report.ts`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as crypto from 'node:crypto';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import { pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
buildOrchestrationLine,
|
|
27
|
+
type Provenance,
|
|
28
|
+
type TierChosen,
|
|
29
|
+
type TierSource,
|
|
30
|
+
type DispatchOutcome,
|
|
31
|
+
type VerifyMode,
|
|
32
|
+
type Band,
|
|
33
|
+
type LinePhase,
|
|
34
|
+
type LineOutcome,
|
|
35
|
+
type RecordInput,
|
|
36
|
+
} from './_lib/orchestration_record.js';
|
|
37
|
+
|
|
38
|
+
const DEFAULT_DIR = 'agents/runtime/state/audit';
|
|
39
|
+
|
|
40
|
+
type Flags = Record<string, string | boolean>;
|
|
41
|
+
|
|
42
|
+
function parseFlags(argv: string[]): Flags {
|
|
43
|
+
const flags: Flags = {};
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const a = argv[i];
|
|
46
|
+
if (a === undefined || !a.startsWith('--')) continue;
|
|
47
|
+
const eq = a.indexOf('=');
|
|
48
|
+
if (eq !== -1) {
|
|
49
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const key = a.slice(2);
|
|
53
|
+
const next = argv[i + 1];
|
|
54
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
55
|
+
flags[key] = next;
|
|
56
|
+
i++;
|
|
57
|
+
} else {
|
|
58
|
+
flags[key] = true; // boolean flag (e.g. --dry-run)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return flags;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function str(flags: Flags, key: string): string | undefined {
|
|
65
|
+
const v = flags[key];
|
|
66
|
+
return typeof v === 'string' ? v : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse an explicit boolean flag value. `--key true|false` → boolean; a bare
|
|
71
|
+
* `--key` counts as `true`; any other value is passed through so the lib's
|
|
72
|
+
* non-boolean validation rejects it with a clear error.
|
|
73
|
+
*/
|
|
74
|
+
function bool(flags: Flags, key: string): boolean | undefined {
|
|
75
|
+
const v = flags[key];
|
|
76
|
+
if (v === undefined) return undefined;
|
|
77
|
+
if (v === true || v === 'true') return true;
|
|
78
|
+
if (v === 'false') return false;
|
|
79
|
+
return v as unknown as boolean; // invalid string → caught by the lib's typeof validation
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function int(flags: Flags, key: string): number | undefined {
|
|
83
|
+
const v = str(flags, key);
|
|
84
|
+
if (v === undefined) return undefined;
|
|
85
|
+
const n = Number(v);
|
|
86
|
+
return Number.isFinite(n) ? n : NaN; // NaN → caught by the lib's isInt validation
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Deterministic content-hash id (contract allows "ULID or content hash"). */
|
|
90
|
+
function contentHashId(input: RecordInput): string {
|
|
91
|
+
const canonical = JSON.stringify({
|
|
92
|
+
ts: input.ts,
|
|
93
|
+
spawn_count: input.spawn_count,
|
|
94
|
+
token_delta: input.token_delta,
|
|
95
|
+
tier_chosen: input.tier_chosen ?? null,
|
|
96
|
+
task_class: input.task_class ?? null,
|
|
97
|
+
});
|
|
98
|
+
return crypto.createHash('sha256').update(canonical).digest('hex').slice(0, 26).toUpperCase();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
102
|
+
const flags = parseFlags(argv);
|
|
103
|
+
const dir = str(flags, 'dir') ?? DEFAULT_DIR;
|
|
104
|
+
const dryRun = flags['dry-run'] === true;
|
|
105
|
+
const ts = new Date().toISOString();
|
|
106
|
+
|
|
107
|
+
const partial: Omit<RecordInput, 'id'> = {
|
|
108
|
+
spawn_count: int(flags, 'spawn-count') ?? NaN,
|
|
109
|
+
token_delta: int(flags, 'token-delta') ?? NaN,
|
|
110
|
+
token_delta_provenance: str(flags, 'provenance') as Provenance | undefined,
|
|
111
|
+
tiers: str(flags, 'tiers')?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
112
|
+
tier_chosen: str(flags, 'tier-chosen') as TierChosen | undefined,
|
|
113
|
+
tier_source: str(flags, 'tier-source') as TierSource | undefined,
|
|
114
|
+
task_class: str(flags, 'task-class'),
|
|
115
|
+
task_size_estimate: int(flags, 'task-size-estimate'),
|
|
116
|
+
wall_clock_ms: int(flags, 'wall-clock-ms'),
|
|
117
|
+
dispatch_tokens: int(flags, 'dispatch-tokens'),
|
|
118
|
+
session_tier: str(flags, 'session-tier'),
|
|
119
|
+
dispatch_outcome: str(flags, 'dispatch-outcome') as DispatchOutcome | undefined,
|
|
120
|
+
verify_mode: str(flags, 'verify-mode') as VerifyMode | undefined,
|
|
121
|
+
first_pass_success: bool(flags, 'first-pass-success'),
|
|
122
|
+
escalated: bool(flags, 'escalated'),
|
|
123
|
+
phase: str(flags, 'phase') as LinePhase | undefined,
|
|
124
|
+
outcome: str(flags, 'outcome') as LineOutcome | undefined,
|
|
125
|
+
confidence_band: str(flags, 'confidence-band') as Band | undefined,
|
|
126
|
+
risk_class: str(flags, 'risk-class') as Band | undefined,
|
|
127
|
+
persona: str(flags, 'persona'),
|
|
128
|
+
work_id: str(flags, 'work-id'),
|
|
129
|
+
ts,
|
|
130
|
+
};
|
|
131
|
+
const input: RecordInput = { ...partial, id: contentHashId({ ...partial, id: '' }) };
|
|
132
|
+
|
|
133
|
+
const { line, errors } = buildOrchestrationLine(input);
|
|
134
|
+
if (errors.length || line === null) {
|
|
135
|
+
process.stderr.write('orchestration_record: refused — invalid input:\n');
|
|
136
|
+
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const serialized = JSON.stringify(line);
|
|
141
|
+
if (dryRun) {
|
|
142
|
+
process.stdout.write(serialized + '\n');
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
147
|
+
const file = path.join(dir, `${ts.slice(0, 7)}.jsonl`);
|
|
148
|
+
fs.appendFileSync(file, serialized + '\n', 'utf8');
|
|
149
|
+
process.stdout.write(`orchestration telemetry recorded → ${file} (id ${line.id})\n`);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (process.argv[1] !== undefined) {
|
|
154
|
+
const invokedUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
155
|
+
if (import.meta.url === invokedUrl) process.exit(main());
|
|
156
|
+
}
|
|
@@ -5,11 +5,18 @@
|
|
|
5
5
|
* and prints an aggregate token-savings report. Read-only; no network, no
|
|
6
6
|
* mutation. Handles a missing/empty audit dir gracefully.
|
|
7
7
|
*
|
|
8
|
+
* QUALITY × COST PAIRING (council verdict): the savings figures never render
|
|
9
|
+
* without the quality columns (first_pass_success_rate / escalation_rate).
|
|
10
|
+
* When ≥ 20 lines in the window carry the quality fields the real rates
|
|
11
|
+
* render; below 20 the columns render as "n/a (n=<count>)" beside savings.
|
|
12
|
+
*
|
|
8
13
|
* Usage:
|
|
9
|
-
* ./scripts-run src/scripts/orchestration_savings_report
|
|
14
|
+
* ./scripts-run src/scripts/orchestration_savings_report \
|
|
15
|
+
* [--dir <path>] [--format text|json] [--weights lite=1,medium=5,high=15]
|
|
10
16
|
*
|
|
11
17
|
* Default dir: agents/runtime/state/audit (the audit-log-v1 path where the
|
|
12
|
-
* orchestration layer writes telemetry lines).
|
|
18
|
+
* orchestration layer writes telemetry lines). `--weights` tunes the
|
|
19
|
+
* provider-neutral tier weights behind the MODELED cost-%. See
|
|
13
20
|
* `src/agent-src/contexts/execution/orchestration-telemetry.md`.
|
|
14
21
|
*/
|
|
15
22
|
|
|
@@ -17,17 +24,30 @@ import * as fs from 'node:fs';
|
|
|
17
24
|
import * as path from 'node:path';
|
|
18
25
|
import { pathToFileURL } from 'node:url';
|
|
19
26
|
|
|
20
|
-
import { aggregateOrchestrationSavings, type AuditLine, type SavingsReport } from './_lib/orchestration_savings.js';
|
|
27
|
+
import { aggregateOrchestrationSavings, DEFAULT_TIER_WEIGHTS, type AuditLine, type SavingsReport, type TierWeights } from './_lib/orchestration_savings.js';
|
|
21
28
|
|
|
22
29
|
const DEFAULT_DIR = 'agents/runtime/state/audit';
|
|
23
30
|
|
|
24
31
|
interface Options {
|
|
25
32
|
dir: string;
|
|
26
33
|
format: 'text' | 'json';
|
|
34
|
+
weights: TierWeights;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parse `lite=1,medium=5,high=15` → weight map. Returns undefined if nothing valid parsed. */
|
|
38
|
+
function parseWeights(spec: string | undefined): TierWeights | undefined {
|
|
39
|
+
if (!spec) return undefined;
|
|
40
|
+
const w: TierWeights = {};
|
|
41
|
+
for (const pair of spec.split(',')) {
|
|
42
|
+
const [k, v] = pair.split('=');
|
|
43
|
+
const n = Number(v);
|
|
44
|
+
if (k && Number.isFinite(n)) w[k.trim()] = n;
|
|
45
|
+
}
|
|
46
|
+
return Object.keys(w).length ? w : undefined;
|
|
27
47
|
}
|
|
28
48
|
|
|
29
49
|
function parseArgs(argv: string[]): Options {
|
|
30
|
-
const opts: Options = { dir: DEFAULT_DIR, format: 'text' };
|
|
50
|
+
const opts: Options = { dir: DEFAULT_DIR, format: 'text', weights: DEFAULT_TIER_WEIGHTS };
|
|
31
51
|
for (let i = 0; i < argv.length; i++) {
|
|
32
52
|
const a = argv[i];
|
|
33
53
|
if (a === undefined) continue;
|
|
@@ -35,6 +55,8 @@ function parseArgs(argv: string[]): Options {
|
|
|
35
55
|
else if (a.startsWith('--dir=')) opts.dir = a.slice('--dir='.length);
|
|
36
56
|
else if (a === '--format') opts.format = (argv[++i] as Options['format']) ?? opts.format;
|
|
37
57
|
else if (a.startsWith('--format=')) opts.format = a.slice('--format='.length) as Options['format'];
|
|
58
|
+
else if (a === '--weights') opts.weights = parseWeights(argv[++i]) ?? opts.weights;
|
|
59
|
+
else if (a.startsWith('--weights=')) opts.weights = parseWeights(a.slice('--weights='.length)) ?? opts.weights;
|
|
38
60
|
}
|
|
39
61
|
if (opts.format !== 'text' && opts.format !== 'json') opts.format = 'text';
|
|
40
62
|
return opts;
|
|
@@ -60,13 +82,41 @@ export function readAuditLines(dir: string): AuditLine[] {
|
|
|
60
82
|
return lines;
|
|
61
83
|
}
|
|
62
84
|
|
|
63
|
-
|
|
85
|
+
/** Dispatches carrying the quality fields at/above this render the real rates; below, "n/a (n=<count>)". */
|
|
86
|
+
export const QUALITY_GATE_MIN_LINES = 20;
|
|
87
|
+
|
|
88
|
+
function pct(v: number): string {
|
|
89
|
+
return `${(v * 100).toFixed(0)}%`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render the paired cost × quality block. Council verdict: savings NEVER render
|
|
94
|
+
* without the quality columns — at n ≥ QUALITY_GATE_MIN_LINES the real rates
|
|
95
|
+
* render; below, the quality columns render as "n/a (n=<count>)" alongside the
|
|
96
|
+
* savings figures. There is no savings-only render path.
|
|
97
|
+
*/
|
|
98
|
+
function renderPairedCostQuality(r: SavingsReport): string[] {
|
|
99
|
+
const q = r.quality;
|
|
100
|
+
const gated = q.quality_lines >= QUALITY_GATE_MIN_LINES;
|
|
101
|
+
const fps = gated && q.first_pass_success_rate !== null
|
|
102
|
+
? `${pct(q.first_pass_success_rate)} (n=${q.first_pass_lines})`
|
|
103
|
+
: `n/a (n=${q.first_pass_lines})`;
|
|
104
|
+
const esc = gated && q.escalation_rate !== null
|
|
105
|
+
? `${pct(q.escalation_rate)} (n=${q.escalated_lines})`
|
|
106
|
+
: `n/a (n=${q.escalated_lines})`;
|
|
107
|
+
return [
|
|
108
|
+
' cost × quality (paired — never savings alone):',
|
|
109
|
+
` net token_delta: ${r.net_token_delta} (negative = net saved) | first_pass_success_rate: ${fps}`,
|
|
110
|
+
` tokens saved: ${r.tokens_saved} · tokens added: ${r.tokens_added} | escalation_rate: ${esc}`,
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderText(r: SavingsReport, dir: string): string {
|
|
64
115
|
const out: string[] = [];
|
|
65
116
|
out.push('Orchestration savings report');
|
|
66
117
|
out.push(` source: ${dir}`);
|
|
67
118
|
out.push(` dispatches: ${r.dispatches} (total spawns: ${r.total_spawns})`);
|
|
68
|
-
out.push(
|
|
69
|
-
out.push(` tokens saved: ${r.tokens_saved} tokens added: ${r.tokens_added}`);
|
|
119
|
+
out.push(...renderPairedCostQuality(r));
|
|
70
120
|
out.push(` provenance: measured ${r.by_provenance.measured.dispatches} (Δ ${r.by_provenance.measured.net_token_delta}) · estimated ${r.by_provenance.estimated.dispatches} (Δ ${r.by_provenance.estimated.net_token_delta})`);
|
|
71
121
|
out.push(` measured share: ${(r.measured_share * 100).toFixed(0)}%`);
|
|
72
122
|
if (Object.keys(r.by_tier).length) {
|
|
@@ -77,6 +127,13 @@ function renderText(r: SavingsReport, dir: string): string {
|
|
|
77
127
|
out.push(' by task_class (Δ tokens):');
|
|
78
128
|
for (const [cls, d] of Object.entries(r.by_task_class).sort()) out.push(` ${cls}: ${d}`);
|
|
79
129
|
}
|
|
130
|
+
const mc = r.modeled_cost;
|
|
131
|
+
if (mc.cost_reduction_pct !== null) {
|
|
132
|
+
out.push(` MODELED cost reduction: ${(mc.cost_reduction_pct * 100).toFixed(0)}% (over ${mc.covered_dispatches}/${r.dispatches} dispatch(es) with tier data)`);
|
|
133
|
+
out.push(` weights ${JSON.stringify(mc.weights)} · baseline ${mc.baseline_cost_units} → delegated ${mc.delegated_cost_units} cost-units · MODEL, not measured $`);
|
|
134
|
+
} else {
|
|
135
|
+
out.push(' MODELED cost reduction: n/a (needs dispatch_tokens + session_tier + tier_chosen on a dispatch)');
|
|
136
|
+
}
|
|
80
137
|
out.push(' notes:');
|
|
81
138
|
for (const n of r.notes) out.push(` - ${n}`);
|
|
82
139
|
return out.join('\n');
|
|
@@ -85,7 +142,7 @@ function renderText(r: SavingsReport, dir: string): string {
|
|
|
85
142
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
86
143
|
const opts = parseArgs(argv);
|
|
87
144
|
const lines = readAuditLines(opts.dir);
|
|
88
|
-
const report = aggregateOrchestrationSavings(lines);
|
|
145
|
+
const report = aggregateOrchestrationSavings(lines, opts.weights);
|
|
89
146
|
if (opts.format === 'json') {
|
|
90
147
|
process.stdout.write(JSON.stringify({ ...report, source: opts.dir }, null, 2) + '\n');
|
|
91
148
|
} else {
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"pattern": "^[a-z][a-z0-9-]*$",
|
|
14
14
|
"description": "Lowercase, hyphenated slug; must match the parent directory name."
|
|
15
15
|
},
|
|
16
|
+
"user-invocable": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"description": "Claude Code Agent-Skills extension: false = the skill claims no /name slash command (only the model can load it). REQUIRED (false) when name equals a Claude Code built-in command or bundled skill (see scripts/_lib/claude_builtin_names.ts) so the suite never shadows the host's own /name. Other tools ignore the key."
|
|
19
|
+
},
|
|
16
20
|
"description": {
|
|
17
21
|
"type": "string",
|
|
18
22
|
"minLength": 1,
|