@event4u/agent-config 16.1.0 → 16.2.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +237 -248
- package/dist/agent-src/commands/roadmap/process-full.md +1 -1
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
- package/dist/agent-src/skills/design-review/SKILL.md +30 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +30 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
- package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
- package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
- package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_conformance.js +902 -782
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +10 -10
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.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 +6666 -2581
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/install.mjs +848 -733
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2747 -1510
- package/dist/server/schemas/settings.js +23 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/contracts/settings-classes.md +6 -4
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +29 -0
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/continuity-surface.json +2 -2
- package/src/config/gate-coverage.yml +234 -3
- package/src/config/gate-violation-baselines.json +7 -3
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +2 -2
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/scripts/_cli/cmd_doctor.ts +5 -19
- package/src/scripts/_cli/doctor_execution.ts +100 -17
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/forge_protection.ts +114 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/obligations.ts +119 -8
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +2 -1
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +245 -3
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_generator_sync.ts +71 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_holds.ts +2 -1
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +115 -1
- package/src/scripts/compile_hook_manifest.ts +60 -16
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +42 -5
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +100 -9
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +1 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +1 -2
- package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +8 -5
- package/src/scripts/hooks/run_continuation_hook.ts +13 -15
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/install.ts +3 -4
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +55 -0
- package/src/scripts/lint_roadmap_blockers.ts +78 -12
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release_publication.ts +13 -1
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +1 -1
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/command.schema.json +44 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -2
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +156 -4
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +31 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/ui/assets/index-DH1xc62v.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Where an artifact declares `inputs:`, its `argument-hint` is DERIVED from
|
|
4
|
+
* that declaration — and a hand-written hint on the same artifact is a
|
|
5
|
+
* conflict, not a merge.
|
|
6
|
+
*
|
|
7
|
+
* road-to-an-invocation-contract-that-reaches-the-wire 3.1.
|
|
8
|
+
*
|
|
9
|
+
* WHY A CONFLICT RATHER THAN A MERGE
|
|
10
|
+
*
|
|
11
|
+
* The tree already answers "what does this take?" in prose, through
|
|
12
|
+
* `argument-hint`, which nothing validates. Adding a structured declaration
|
|
13
|
+
* beside it creates a SECOND authority for the same question, and two surfaces
|
|
14
|
+
* that can answer one question drift — which is the failure this whole roadmap
|
|
15
|
+
* exists to make unrepresentable (Risk 1). Making the derived direction
|
|
16
|
+
* one-way, and a disagreement a build failure, is what keeps the second
|
|
17
|
+
* authority from forming. Merging the two would BE the second authority.
|
|
18
|
+
*
|
|
19
|
+
* An artifact with no declaration is untouched: its hand-written hint stays
|
|
20
|
+
* exactly as it is, because there is nothing to derive from and no conflict to
|
|
21
|
+
* have. That is why the 43 hintless commands are a generation gap that closes
|
|
22
|
+
* as declarations land, rather than 43 prose edits nobody reviewed.
|
|
23
|
+
*
|
|
24
|
+
* THE FORM, AND WHY IT ROUND-TRIPS
|
|
25
|
+
*
|
|
26
|
+
* `<name>` required · `[name]` optional · `:a|b` appended for an enum. The
|
|
27
|
+
* corpus already writes `[path]` and `[--force]` in exactly this shape, so the
|
|
28
|
+
* generated hint is not a new dialect. {@link parseHint} recovers the name,
|
|
29
|
+
* requiredness and enum from the rendered string, and the round-trip is
|
|
30
|
+
* asserted rather than assumed — a generator whose output its own parser cannot
|
|
31
|
+
* read would make the conflict check unfalsifiable.
|
|
32
|
+
*
|
|
33
|
+
* Exit codes: 0 in sync · 1 a conflict or a missing derived hint · 2 unreadable.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import fs from 'node:fs';
|
|
37
|
+
import os from 'node:os';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
|
+
|
|
41
|
+
import * as yaml from 'js-yaml';
|
|
42
|
+
|
|
43
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
44
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
45
|
+
import { assertScanned, reportScanned } from './_lib/scan_scope.js';
|
|
46
|
+
|
|
47
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
48
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
49
|
+
|
|
50
|
+
export const GATE = 'check_argument_hint';
|
|
51
|
+
|
|
52
|
+
export interface InputDecl {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly type: string;
|
|
55
|
+
readonly required?: boolean;
|
|
56
|
+
readonly enum?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Render the canonical hint for a declaration. */
|
|
60
|
+
export function hintFor(inputs: readonly InputDecl[]): string {
|
|
61
|
+
return inputs
|
|
62
|
+
.map((i) => {
|
|
63
|
+
const body = i.enum && i.enum.length > 0 ? `${i.name}:${i.enum.join('|')}` : i.name;
|
|
64
|
+
return i.required === true ? `<${body}>` : `[${body}]`;
|
|
65
|
+
})
|
|
66
|
+
.join(' ');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ParsedHint {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly required: boolean;
|
|
72
|
+
readonly enum?: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Recover declarations from a rendered hint. The inverse of {@link hintFor}. */
|
|
76
|
+
export function parseHint(hint: string): ParsedHint[] {
|
|
77
|
+
const out: ParsedHint[] = [];
|
|
78
|
+
for (const m of hint.matchAll(/([<[])([a-z][a-z0-9_]*)(?::([^\]>]+))?([\]>])/g)) {
|
|
79
|
+
const open = m[1]!;
|
|
80
|
+
const close = m[4]!;
|
|
81
|
+
if ((open === '<') !== (close === '>')) continue;
|
|
82
|
+
const parsed: ParsedHint = {
|
|
83
|
+
name: m[2]!,
|
|
84
|
+
required: open === '<',
|
|
85
|
+
...(m[3] === undefined ? {} : { enum: m[3].split('|') }),
|
|
86
|
+
};
|
|
87
|
+
out.push(parsed);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Frontmatter of one artifact, or `null` when it has none or it is malformed. */
|
|
93
|
+
function frontmatter(text: string): Record<string, unknown> | null {
|
|
94
|
+
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
95
|
+
if (m === null) return null;
|
|
96
|
+
try {
|
|
97
|
+
const fm = yaml.load(m[1]!);
|
|
98
|
+
return fm !== null && typeof fm === 'object' ? (fm as Record<string, unknown>) : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function declaredInputs(fm: Record<string, unknown> | null): InputDecl[] | null {
|
|
105
|
+
if (fm === null) return null;
|
|
106
|
+
const raw = fm['inputs'];
|
|
107
|
+
if (!Array.isArray(raw)) return null;
|
|
108
|
+
const out: InputDecl[] = [];
|
|
109
|
+
for (const item of raw) {
|
|
110
|
+
if (item === null || typeof item !== 'object') continue;
|
|
111
|
+
const r = item as Record<string, unknown>;
|
|
112
|
+
if (typeof r['name'] !== 'string' || typeof r['type'] !== 'string') continue;
|
|
113
|
+
out.push({
|
|
114
|
+
name: r['name'],
|
|
115
|
+
type: r['type'],
|
|
116
|
+
...(typeof r['required'] === 'boolean' ? { required: r['required'] } : {}),
|
|
117
|
+
...(Array.isArray(r['enum'])
|
|
118
|
+
? { enum: r['enum'].filter((x): x is string => typeof x === 'string') }
|
|
119
|
+
: {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type FindingKind = 'conflict' | 'missing';
|
|
126
|
+
|
|
127
|
+
export interface Finding {
|
|
128
|
+
readonly file: string;
|
|
129
|
+
readonly kind: FindingKind;
|
|
130
|
+
readonly derived: string;
|
|
131
|
+
/** The hand-written hint, for a conflict. */
|
|
132
|
+
readonly written?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Compare one artifact's declaration against its hint. */
|
|
136
|
+
export function checkArtifact(text: string, file: string): Finding | null {
|
|
137
|
+
const fm = frontmatter(text);
|
|
138
|
+
const inputs = declaredInputs(fm);
|
|
139
|
+
if (inputs === null || inputs.length === 0) return null;
|
|
140
|
+
const derived = hintFor(inputs);
|
|
141
|
+
const written = fm?.['argument-hint'];
|
|
142
|
+
if (typeof written !== 'string') return { file, kind: 'missing', derived };
|
|
143
|
+
if (written.trim() !== derived) return { file, kind: 'conflict', derived, written };
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function walk(dir: string, match: (n: string) => boolean, out: string[] = []): string[] {
|
|
148
|
+
if (!fs.existsSync(dir)) return out;
|
|
149
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
150
|
+
const p = path.join(dir, e.name);
|
|
151
|
+
if (e.isDirectory()) walk(p, match, out);
|
|
152
|
+
else if (match(e.name)) out.push(p);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* COMMANDS ONLY, and the reason is structural rather than a scoping preference:
|
|
159
|
+
* `argument-hint` is a key of `command.schema.json` and of no other schema. A
|
|
160
|
+
* skill cannot carry one, so reporting a skill that declares `inputs:` as
|
|
161
|
+
* "missing its hint" would demand a field the schema forbids. Skills still get
|
|
162
|
+
* their declaration onto the wire — that is `to_mcp_prompt_meta`'s job (3.2),
|
|
163
|
+
* which reads the declaration directly and needs no hint at all.
|
|
164
|
+
*/
|
|
165
|
+
export function scan(root: string): { findings: Finding[]; scanned: number; declaring: number } {
|
|
166
|
+
const files = walk(path.join(root, 'src', 'domains'), (n) => n === 'command.md').sort();
|
|
167
|
+
|
|
168
|
+
const findings: Finding[] = [];
|
|
169
|
+
let declaring = 0;
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
const text = fs.readFileSync(f, 'utf8');
|
|
172
|
+
const inputs = declaredInputs(frontmatter(text));
|
|
173
|
+
if (inputs !== null && inputs.length > 0) declaring += 1;
|
|
174
|
+
const finding = checkArtifact(text, path.relative(root, f));
|
|
175
|
+
if (finding !== null) findings.push(finding);
|
|
176
|
+
}
|
|
177
|
+
return { findings, scanned: files.length, declaring };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function main(argv: readonly string[]): number {
|
|
181
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
182
|
+
const rootIdx = argv.indexOf('--root');
|
|
183
|
+
const root = rootIdx === -1 ? ROOT : (argv[rootIdx + 1] ?? ROOT);
|
|
184
|
+
|
|
185
|
+
const { findings, scanned, declaring } = scan(root);
|
|
186
|
+
const ledger = new GateLedger(GATE);
|
|
187
|
+
ledger.plan(findings.map((f) => f.file));
|
|
188
|
+
for (const f of findings) ledger.fail(f.file, f.kind);
|
|
189
|
+
|
|
190
|
+
// A gate that read nothing has not passed. This fires BEFORE the verdict,
|
|
191
|
+
// because certifying against an empty corpus is the false green the coverage
|
|
192
|
+
// manifest exists to refuse — named by the provider-diverse ratification
|
|
193
|
+
// review as one condition that would have flipped its verdict.
|
|
194
|
+
assertScanned({ gate: GATE, scanned: scanned, units: 'artifact(s)', roots: ['src/domains'] });
|
|
195
|
+
|
|
196
|
+
reportScanned({
|
|
197
|
+
gate: GATE,
|
|
198
|
+
scanned,
|
|
199
|
+
units: 'artifact(s)',
|
|
200
|
+
roots: ['src/domains'],
|
|
201
|
+
});
|
|
202
|
+
ledger.report();
|
|
203
|
+
|
|
204
|
+
if (findings.length > 0) {
|
|
205
|
+
for (const f of findings) {
|
|
206
|
+
if (f.kind === 'conflict') {
|
|
207
|
+
process.stdout.write(
|
|
208
|
+
`❌ ${GATE}: ${f.file} has a hand-written \`argument-hint\` that contradicts ` +
|
|
209
|
+
`its declaration.\n written: ${f.written ?? ''}\n derived: ${f.derived}\n`,
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
process.stdout.write(
|
|
213
|
+
`❌ ${GATE}: ${f.file} declares \`inputs:\` but carries no \`argument-hint\`.\n` +
|
|
214
|
+
` derived: ${f.derived}\n`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
process.stdout.write(
|
|
219
|
+
'\nWhere a declaration exists the hint is derived from it, one way. Fix the ' +
|
|
220
|
+
'declaration, or delete the hand-written hint — never reconcile the two by ' +
|
|
221
|
+
'hand, which is the second authority this check exists to prevent.\n',
|
|
222
|
+
);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
process.stdout.write(
|
|
227
|
+
`✅ ${GATE}: every derived \`argument-hint\` matches its declaration ` +
|
|
228
|
+
`(${String(declaring)} of ${String(scanned)} artifact(s) declare \`inputs:\`).\n`,
|
|
229
|
+
);
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A self-test is what proves a gate DISCRIMINATES. An enforced scan floor only
|
|
235
|
+
* proves it read something; the verdict has to move.
|
|
236
|
+
*/
|
|
237
|
+
function selfTest(): number {
|
|
238
|
+
const plant = (name: string, fm: string): string => {
|
|
239
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `arg-hint-${name}-`));
|
|
240
|
+
const cmd = path.join(dir, 'src', 'domains', 'probe-pack', 'probe');
|
|
241
|
+
fs.mkdirSync(cmd, { recursive: true });
|
|
242
|
+
fs.writeFileSync(path.join(cmd, 'command.md'), `---\nname: probe\n${fm}---\n\nbody\n`, 'utf-8');
|
|
243
|
+
return dir;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const run = (root: string): number =>
|
|
247
|
+
runGateCli(ROOT, path.join('src', 'scripts', 'check_argument_hint.ts'), ['--root', root], ROOT);
|
|
248
|
+
|
|
249
|
+
return runSelfTest({
|
|
250
|
+
gate: GATE,
|
|
251
|
+
minCases: 5,
|
|
252
|
+
minRejectCases: 3,
|
|
253
|
+
cases: [
|
|
254
|
+
{
|
|
255
|
+
name: 'an empty corpus is refused, not certified green',
|
|
256
|
+
expect: 'reject',
|
|
257
|
+
run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'arg-hint-empty-'))),
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'a hint matching its declaration passes',
|
|
261
|
+
expect: 'accept',
|
|
262
|
+
run: () =>
|
|
263
|
+
run(plant('match', 'argument-hint: "<a>"\ninputs:\n - name: a\n type: string\n required: true\n')),
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'a hand-written hint contradicting the declaration fails',
|
|
267
|
+
expect: 'reject',
|
|
268
|
+
run: () =>
|
|
269
|
+
run(plant('conflict', 'argument-hint: "[other]"\ninputs:\n - name: a\n type: string\n required: true\n')),
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'a declaration with no hint at all fails',
|
|
273
|
+
expect: 'reject',
|
|
274
|
+
run: () => run(plant('missing', 'inputs:\n - name: a\n type: string\n')),
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'an artifact with no declaration keeps its free-text hint',
|
|
278
|
+
expect: 'accept',
|
|
279
|
+
run: () => run(plant('nodecl', 'argument-hint: "[free text]"\n')),
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
286
|
+
process.exit(main(process.argv.slice(2)));
|
|
287
|
+
}
|
|
@@ -208,9 +208,76 @@ const SELF_COUNT = new RegExp(
|
|
|
208
208
|
'i',
|
|
209
209
|
);
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Byte-unit metric ids, read from `src/config/metric-registry.yml`.
|
|
213
|
+
*
|
|
214
|
+
* WHY THE IDS AND NOT THE WORD `bytes`. A detector keyed on a number near the
|
|
215
|
+
* word would red on ordinary documentation — a file size in a README, a payload
|
|
216
|
+
* example in a contract — and the cheapest repair for a gate that reds on
|
|
217
|
+
* correct prose is to weaken it until it finds nothing. Keyed on the DECLARED
|
|
218
|
+
* ids, only a figure claiming a registered metric is checked, and a line that
|
|
219
|
+
* merely mentions bytes is not a claim about this package at all.
|
|
220
|
+
*
|
|
221
|
+
* `MAGNITUDE` already covers `KB|MB|GB`, so this is not the first byte-shaped
|
|
222
|
+
* figure the gate sees; what it adds is the id-bearing form, which is how
|
|
223
|
+
* `road-to-a-bytes-row-that-exists` made a byte statement expressible in the
|
|
224
|
+
* first place.
|
|
225
|
+
*
|
|
226
|
+
* Parsed with a line scanner rather than a YAML dependency: the shape read is
|
|
227
|
+
* two adjacent keys at a fixed indent, and an unreadable registry yields an
|
|
228
|
+
* EMPTY id list, which makes the detector inert rather than throwing inside a
|
|
229
|
+
* gate whose job is to report other people's problems.
|
|
230
|
+
*/
|
|
231
|
+
export function parse_byte_metric_ids(raw: string): string[] {
|
|
232
|
+
const out: string[] = [];
|
|
233
|
+
let current: string | null = null;
|
|
234
|
+
for (const line of raw.split('\n')) {
|
|
235
|
+
const id = /^\s*-\s+id:\s*([A-Za-z0-9._-]+)\s*$/.exec(line);
|
|
236
|
+
if (id?.[1] !== undefined) {
|
|
237
|
+
current = id[1];
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const unit = /^\s*unit:\s*(\S+)\s*$/.exec(line);
|
|
241
|
+
if (unit?.[1] !== undefined && current !== null && /^bytes/.test(unit[1])) {
|
|
242
|
+
out.push(current);
|
|
243
|
+
current = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const METRIC_REGISTRY_REL = 'src/config/metric-registry.yml';
|
|
250
|
+
let _byteMetricIds: string[] | null = null;
|
|
251
|
+
|
|
252
|
+
function byteMetricIds(): string[] {
|
|
253
|
+
if (_byteMetricIds === null) {
|
|
254
|
+
try {
|
|
255
|
+
_byteMetricIds = parse_byte_metric_ids(
|
|
256
|
+
fs.readFileSync(path.join(REPO, METRIC_REGISTRY_REL), 'utf-8'),
|
|
257
|
+
);
|
|
258
|
+
} catch {
|
|
259
|
+
_byteMetricIds = [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return _byteMetricIds;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A line naming a declared byte metric AND carrying a figure.
|
|
267
|
+
*
|
|
268
|
+
* Both halves are required. The id alone is prose about the metric — the
|
|
269
|
+
* registry entry itself says what it is for, and that is not a claim. A number
|
|
270
|
+
* alone is every other line in the tree.
|
|
271
|
+
*/
|
|
272
|
+
export function is_byte_metric_claim(line: string, ids: string[] = byteMetricIds()): boolean {
|
|
273
|
+
if (ids.length === 0) return false;
|
|
274
|
+
if (!/\b\d[\d,._]*\b/.test(line)) return false;
|
|
275
|
+
return ids.some((id) => new RegExp(String.raw`\b${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\b`).test(line));
|
|
276
|
+
}
|
|
277
|
+
|
|
211
278
|
/** True when a line carries any figure shape that must bind to a claim. */
|
|
212
279
|
export function is_quantified_claim(line: string): boolean {
|
|
213
|
-
return RATIO.test(line) || MAGNITUDE.test(line) || SELF_COUNT.test(line);
|
|
280
|
+
return RATIO.test(line) || MAGNITUDE.test(line) || SELF_COUNT.test(line) || is_byte_metric_claim(line);
|
|
214
281
|
}
|
|
215
282
|
|
|
216
283
|
class ExitCode extends Error {
|
|
@@ -1041,6 +1108,17 @@ function selfTest(): number {
|
|
|
1041
1108
|
|
|
1042
1109
|
const CLEAN_README = '# Agent Config\n\nGoverned skills, rules and commands.\n';
|
|
1043
1110
|
|
|
1111
|
+
/** A registry carrying exactly one byte-unit metric, for the 4.1 cases. */
|
|
1112
|
+
const BYTE_REGISTRY = [
|
|
1113
|
+
'metrics:',
|
|
1114
|
+
' - id: tool-raw-bytes',
|
|
1115
|
+
' unit: bytes',
|
|
1116
|
+
' basis: measured',
|
|
1117
|
+
' - id: some-token-metric',
|
|
1118
|
+
' unit: tokens',
|
|
1119
|
+
'',
|
|
1120
|
+
].join('\n');
|
|
1121
|
+
|
|
1044
1122
|
const cases: SelfTestCase[] = [
|
|
1045
1123
|
{
|
|
1046
1124
|
name: 'a retired phrasing absent from every publish surface → accept',
|
|
@@ -1071,6 +1149,52 @@ function selfTest(): number {
|
|
|
1071
1149
|
expect: 'reject',
|
|
1072
1150
|
run: () => fixture({ 'docs/CLAIMS.md': closed('daemon'), 'README.md': CLEAN_README }),
|
|
1073
1151
|
},
|
|
1152
|
+
|
|
1153
|
+
// road-to-a-bytes-row-that-exists 4.1 — both directions, because a
|
|
1154
|
+
// one-directional fixture cannot tell a working detector from one that
|
|
1155
|
+
// reds on everything. The registry file is part of the fixture: the
|
|
1156
|
+
// gate reads it relative to its own location, so the copied script in
|
|
1157
|
+
// the temp root reads the copied registry.
|
|
1158
|
+
{
|
|
1159
|
+
name: 'a byte figure naming a metric with no ledger entry → reject',
|
|
1160
|
+
expect: 'reject',
|
|
1161
|
+
run: () =>
|
|
1162
|
+
fixture({
|
|
1163
|
+
'docs/CLAIMS.md': closed(null),
|
|
1164
|
+
'src/config/metric-registry.yml': BYTE_REGISTRY,
|
|
1165
|
+
'README.md': `${CLEAN_README}\nEach dispatch records 2048 tool-raw-bytes.\n`,
|
|
1166
|
+
}),
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: 'the same figure once a markered ledger entry backs it → accept',
|
|
1170
|
+
expect: 'accept',
|
|
1171
|
+
run: () =>
|
|
1172
|
+
fixture({
|
|
1173
|
+
'docs/CLAIMS.md': [
|
|
1174
|
+
closed(null),
|
|
1175
|
+
'### claim: tool-raw-bytes-observed',
|
|
1176
|
+
'- claim: Each dispatch records 2048 tool-raw-bytes.',
|
|
1177
|
+
'- kind: quant',
|
|
1178
|
+
'- evidence: docs/evidence.md#ANCHOR',
|
|
1179
|
+
'- status: backed',
|
|
1180
|
+
'- last_verified: 2026-09-29',
|
|
1181
|
+
'',
|
|
1182
|
+
].join('\n'),
|
|
1183
|
+
'src/config/metric-registry.yml': BYTE_REGISTRY,
|
|
1184
|
+
'README.md':
|
|
1185
|
+
`${CLEAN_README}\nEach dispatch records 2048 tool-raw-bytes. <!-- claim:tool-raw-bytes-observed -->\n`,
|
|
1186
|
+
}),
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
name: 'ordinary prose carrying a byte size but no declared id → accept',
|
|
1190
|
+
expect: 'accept',
|
|
1191
|
+
run: () =>
|
|
1192
|
+
fixture({
|
|
1193
|
+
'docs/CLAIMS.md': closed(null),
|
|
1194
|
+
'src/config/metric-registry.yml': BYTE_REGISTRY,
|
|
1195
|
+
'README.md': `${CLEAN_README}\nThe example payload below is 2048 bytes long.\n`,
|
|
1196
|
+
}),
|
|
1197
|
+
},
|
|
1074
1198
|
{
|
|
1075
1199
|
name: 'the never-published sentinel with a stated reason → accept',
|
|
1076
1200
|
expect: 'accept',
|
|
@@ -260,13 +260,22 @@ function load_corpus(dirs: readonly string[], files: readonly string[] = []): st
|
|
|
260
260
|
* whether or not anyone chooses to. Conflating the two is how a package ends up
|
|
261
261
|
* believing an on-demand tool is a gate.
|
|
262
262
|
*/
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Transitive closure of the scripts a wiring surface reaches.
|
|
265
|
+
*
|
|
266
|
+
* `roots` is a seam, not a feature: it defaults to the real tree, and a test
|
|
267
|
+
* passes a synthetic one. Without it the only way to exercise the fixed point
|
|
268
|
+
* is over ~650 real files, where a chain deep enough to catch a broken worklist
|
|
269
|
+
* cannot be constructed on purpose.
|
|
270
|
+
*/
|
|
271
|
+
export function reachable_scripts(wiring: string, roots?: { dir: string; base: string }): Set<string> {
|
|
272
|
+
const scripts_dir = roots?.dir ?? path.join(REPO_ROOT, 'src', 'scripts');
|
|
273
|
+
const base_dir = roots?.base ?? REPO_ROOT;
|
|
265
274
|
const all: string[] = fs.existsSync(scripts_dir)
|
|
266
275
|
? walk(scripts_dir).filter((p) => p.endsWith('.ts'))
|
|
267
276
|
: [];
|
|
268
277
|
|
|
269
|
-
const rel = (abs: string): string => path.relative(
|
|
278
|
+
const rel = (abs: string): string => path.relative(base_dir, abs);
|
|
270
279
|
const stem = (abs: string): string => path.basename(abs).replace(/\.ts$/, '');
|
|
271
280
|
|
|
272
281
|
const reached = new Set<string>();
|
|
@@ -274,24 +283,44 @@ function reachable_scripts(wiring: string): Set<string> {
|
|
|
274
283
|
if (wiring.includes(rel(abs)) || wiring.includes(stem(abs))) reached.add(rel(abs));
|
|
275
284
|
}
|
|
276
285
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
286
|
+
// WORKLIST, not a re-scan of everything reached so far.
|
|
287
|
+
//
|
|
288
|
+
// The previous shape rebuilt `bodies` from EVERY reached file on every
|
|
289
|
+
// iteration and re-tested every unreached file against the whole thing. The
|
|
290
|
+
// corpus is ~650 scripts, `mentions_as_code` compiles and runs two regexes
|
|
291
|
+
// per candidate, and the concatenated body grows into the megabytes — so the
|
|
292
|
+
// cost was iterations x unreached x |all bodies so far|, with each iteration
|
|
293
|
+
// re-doing every comparison the previous ones had already made.
|
|
294
|
+
//
|
|
295
|
+
// Measured, because this was found by profile rather than by reading:
|
|
296
|
+
// `build_proof.render()` -- which calls this transitively -- took 241 s on a
|
|
297
|
+
// developer machine under plain `tsx`, and the CPU profile attributed
|
|
298
|
+
// essentially all of it to this function's regex sweep, spread over hundreds
|
|
299
|
+
// of per-name `RegExp` frames. The file's own header had recorded ~54 s.
|
|
300
|
+
//
|
|
301
|
+
// Re-testing an old body is provably redundant: a file that did not match a
|
|
302
|
+
// body in the iteration that body was added cannot match it later, because
|
|
303
|
+
// neither the body nor the test changes. So each round searches ONLY the
|
|
304
|
+
// bodies admitted in the previous round. Same fixed point, same result --
|
|
305
|
+
// the loop still runs until nothing new is reached.
|
|
306
|
+
let frontier = [...reached];
|
|
307
|
+
while (frontier.length > 0) {
|
|
281
308
|
const bodies = frontier
|
|
282
309
|
.map((r) => {
|
|
283
|
-
const abs = path.join(
|
|
310
|
+
const abs = path.join(base_dir, r);
|
|
284
311
|
return fs.existsSync(abs) ? strip_comments(fs.readFileSync(abs, 'utf-8')) : '';
|
|
285
312
|
})
|
|
286
313
|
.join('\n');
|
|
314
|
+
const next: string[] = [];
|
|
287
315
|
for (const abs of all) {
|
|
288
316
|
const r = rel(abs);
|
|
289
317
|
if (reached.has(r)) continue;
|
|
290
318
|
if (mentions_as_code(bodies, path.basename(abs), stem(abs))) {
|
|
291
319
|
reached.add(r);
|
|
292
|
-
|
|
320
|
+
next.push(r);
|
|
293
321
|
}
|
|
294
322
|
}
|
|
323
|
+
frontier = next;
|
|
295
324
|
}
|
|
296
325
|
return reached;
|
|
297
326
|
}
|