@event4u/agent-config 12.0.0 → 13.0.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 +273 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +41 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- 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 +1 -1
- package/dist/cli-delegate/cmd_prune.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 +44 -23
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +10625 -8541
- package/dist/install/install.mjs +754 -484
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1302 -167
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/MIGRATION.md +20 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +63 -11
- package/src/config/gate-violation-baselines.json +12 -0
- package/src/config/hook-token-budget.json +15 -2
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +1115 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +197 -18
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +279 -317
- package/src/scripts/check_branch_freshness.ts +7 -1
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +50 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +75 -15
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_scheduled_deprecations.ts +738 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +100 -6
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +3 -3
- package/src/scripts/schemas/skill.schema.json +20 -2
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +2 -1
- package/src/server/schemas/settings.ts +2 -2
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
- package/src/config/budget-routing.json +0 -7
|
@@ -19,10 +19,24 @@
|
|
|
19
19
|
* frontmatter keys it carries, and its position in the sorted catalogue. This
|
|
20
20
|
* is the host's INPUT and it is fully file-measurable.
|
|
21
21
|
*
|
|
22
|
-
* OBSERVATION
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* OBSERVATION — which entries actually arrived, and how many did not. This
|
|
23
|
+
* side has TWO sources and every record says which one it came from:
|
|
24
|
+
*
|
|
25
|
+
* `self-report` — an agent reading its own context lists the bare names.
|
|
26
|
+
* Only the agent can see this. It is not enforcement and nothing here
|
|
27
|
+
* pretends otherwise. This is the claude-shaped path.
|
|
28
|
+
*
|
|
29
|
+
* `host-event` — the host publishes its own truncation on a machine
|
|
30
|
+
* channel. `codex exec --json` emits an `item.completed` error event whose
|
|
31
|
+
* message states how many entries it dropped, so the count is READ, never
|
|
32
|
+
* transcribed. This is the codex-shaped path and it is deterministic.
|
|
33
|
+
*
|
|
34
|
+
* Hosts do not truncate the same way, so a record also carries a
|
|
35
|
+
* `truncation_mode`: `per-entry` (some entries arrive bare, others full —
|
|
36
|
+
* claude) versus `budget-strip-and-drop` (every description stripped, then N
|
|
37
|
+
* entries dropped wholesale — codex). Pooling the two into one verdict would
|
|
38
|
+
* average two different mechanisms into a number describing neither, so the
|
|
39
|
+
* report is always per host.
|
|
26
40
|
*
|
|
27
41
|
* The join of the two answers the question the census could not: not "how many
|
|
28
42
|
* are bare" but WHICH PROPERTY separates bare from described. That is the
|
|
@@ -33,370 +47,318 @@
|
|
|
33
47
|
* while an earlier one was bare, which no head-N budget explains.
|
|
34
48
|
*
|
|
35
49
|
* PRIVACY BY CONSTRUCTION. An observation record carries skill names, integer
|
|
36
|
-
* counts
|
|
37
|
-
* text, file bodies, paths outside the catalogue, or user
|
|
38
|
-
* shape `domain-safety-pii` § Surface 2 requires of a log
|
|
39
|
-
*
|
|
50
|
+
* counts, a host label and two closed enums. The record type has no field able
|
|
51
|
+
* to hold prompt text, file bodies, paths outside the catalogue, or user
|
|
52
|
+
* content — the same shape `domain-safety-pii` § Surface 2 requires of a log
|
|
53
|
+
* event. The codex path reads a host message and keeps only the integer in it;
|
|
54
|
+
* the message text itself never reaches a record. Do not widen it with a
|
|
55
|
+
* free-form field.
|
|
56
|
+
*
|
|
57
|
+
* A ZERO IS NEVER INFERRED FROM SILENCE. If the host's budget event is absent
|
|
58
|
+
* or unparseable — reworded upstream, or removed — the capture fails loudly
|
|
59
|
+
* rather than recording `dropped: 0`, because a broken parser and a fixed
|
|
60
|
+
* defect would otherwise look identical. Recording a genuine no-truncation run
|
|
61
|
+
* takes an explicit `--assert-no-truncation`.
|
|
40
62
|
*
|
|
41
|
-
* Exit codes: 0 report produced · 1 usage/IO error
|
|
42
|
-
* (a catalogue scan that found nothing must never read as a clean result)
|
|
63
|
+
* Exit codes: 0 report produced · 1 usage/IO error, an empty catalogue root
|
|
64
|
+
* (a catalogue scan that found nothing must never read as a clean result), or
|
|
65
|
+
* an unreadable host observation.
|
|
43
66
|
*/
|
|
44
67
|
|
|
45
68
|
import * as fs from 'node:fs';
|
|
69
|
+
import * as os from 'node:os';
|
|
46
70
|
import * as path from 'node:path';
|
|
47
71
|
import { fileURLToPath } from 'node:url';
|
|
48
72
|
|
|
73
|
+
import {
|
|
74
|
+
DEFAULT_CATALOGUE_ROOTS,
|
|
75
|
+
OBSERVATION_LOG,
|
|
76
|
+
analyzeSelector,
|
|
77
|
+
buildHostEventRecord,
|
|
78
|
+
buildNoTruncationRecord,
|
|
79
|
+
buildObservationRecord,
|
|
80
|
+
classifyHostProjection,
|
|
81
|
+
formatPerHostVerdicts,
|
|
82
|
+
formatProjectionModes,
|
|
83
|
+
formatReport,
|
|
84
|
+
knownHostLimits,
|
|
85
|
+
measureCatalogueVolume,
|
|
86
|
+
parseHostBudgetEvent,
|
|
87
|
+
readObservationLog,
|
|
88
|
+
readProjectedCatalogue,
|
|
89
|
+
type HostProjectionRow,
|
|
90
|
+
type ProjectionMode,
|
|
91
|
+
type ProjectionModeCounts,
|
|
92
|
+
type SelectorReport,
|
|
93
|
+
} from './_lib/skill_catalogue.js';
|
|
94
|
+
import { scoped_projection_stats } from './_lib/scoped_projection.js';
|
|
95
|
+
import { iter_skills } from './update_counts.js';
|
|
96
|
+
|
|
97
|
+
// Re-exported so the CLI module stays the one public name for this tool.
|
|
98
|
+
export * from './_lib/skill_catalogue.js';
|
|
99
|
+
|
|
49
100
|
const _HERE = fileURLToPath(import.meta.url);
|
|
50
101
|
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
51
102
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/** 1-based position in the alphabetically sorted catalogue. */
|
|
67
|
-
position: number;
|
|
68
|
-
/** Whether the projected SKILL.md declares a `description:`. */
|
|
69
|
-
hasDescription: boolean;
|
|
70
|
-
/** Character length of the declared description; 0 when absent. */
|
|
71
|
-
descriptionLength: number;
|
|
72
|
-
/** Top-level frontmatter keys the entry declares. */
|
|
73
|
-
frontmatterKeys: string[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface SelectorCandidate {
|
|
77
|
-
/** Stable identifier for the property under test. */
|
|
78
|
-
id: string;
|
|
79
|
-
/** Human-readable statement of what the property is. */
|
|
80
|
-
describes: string;
|
|
81
|
-
/** True when the property's values are disjoint across the two groups. */
|
|
82
|
-
separates: boolean;
|
|
83
|
-
/** Why it does or does not separate, in one line. */
|
|
84
|
-
detail: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface SelectorReport {
|
|
88
|
-
catalogueRoot: string;
|
|
89
|
-
entriesTotal: number;
|
|
90
|
-
observedBare: string[];
|
|
91
|
-
observedDescribed: string[];
|
|
92
|
-
/** Observed names that the projection does not know. */
|
|
93
|
-
unknownObserved: string[];
|
|
94
|
-
candidates: SelectorCandidate[];
|
|
95
|
-
verdict: 'selector-found' | 'no-selector' | 'insufficient-observation';
|
|
103
|
+
function resolveCatalogueRoot(explicit: string | null): string {
|
|
104
|
+
if (explicit) {
|
|
105
|
+
if (!fs.existsSync(explicit)) {
|
|
106
|
+
throw new Error(`catalogue root does not exist: ${explicit}`);
|
|
107
|
+
}
|
|
108
|
+
return explicit;
|
|
109
|
+
}
|
|
110
|
+
for (const candidate of DEFAULT_CATALOGUE_ROOTS) {
|
|
111
|
+
const abs = path.join(REPO, candidate);
|
|
112
|
+
if (fs.existsSync(abs)) return abs;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(
|
|
115
|
+
`no catalogue root found — tried ${DEFAULT_CATALOGUE_ROOTS.join(', ')} under ${REPO}`,
|
|
116
|
+
);
|
|
96
117
|
}
|
|
97
118
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
if (end === -1) return '';
|
|
103
|
-
return content.slice(3, end);
|
|
119
|
+
function argValue(flag: string): string | null {
|
|
120
|
+
const index = process.argv.indexOf(flag);
|
|
121
|
+
if (index === -1 || index + 1 >= process.argv.length) return null;
|
|
122
|
+
return process.argv[index + 1]!;
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
function topLevelKeys(frontmatter: string): string[] {
|
|
112
|
-
const keys: string[] = [];
|
|
113
|
-
for (const line of frontmatter.split('\n')) {
|
|
114
|
-
const match = /^([A-Za-z_][A-Za-z0-9_-]*):/.exec(line);
|
|
115
|
-
if (match) keys.push(match[1]!);
|
|
125
|
+
/** Every occurrence of a repeatable flag, in argv order. */
|
|
126
|
+
function argValues(flag: string): string[] {
|
|
127
|
+
const out: string[] = [];
|
|
128
|
+
for (let i = 0; i < process.argv.length - 1; i += 1) {
|
|
129
|
+
if (process.argv[i] === flag) out.push(process.argv[i + 1]!);
|
|
116
130
|
}
|
|
117
|
-
return
|
|
131
|
+
return out;
|
|
118
132
|
}
|
|
119
133
|
|
|
120
134
|
/**
|
|
121
|
-
*
|
|
135
|
+
* `--projection-mode <scoped|legacy-all>`, validated against the closed enum.
|
|
122
136
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* reported or refuted on it. Block scalars are therefore folded properly.
|
|
137
|
+
* Returns `undefined` when the flag is absent — which the record schema reads
|
|
138
|
+
* as "not captured", never as `legacy-all`. An unrecognised value throws
|
|
139
|
+
* rather than falling back: a typo silently recorded as the wrong mode is the
|
|
140
|
+
* failure this whole module refuses elsewhere.
|
|
128
141
|
*/
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (/^[|>][-+]?\d*$/.test(rest)) {
|
|
137
|
-
const body: string[] = [];
|
|
138
|
-
for (let j = i + 1; j < lines.length; j += 1) {
|
|
139
|
-
const line = lines[j]!;
|
|
140
|
-
if (line.trim() === '') {
|
|
141
|
-
body.push('');
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (!/^\s/.test(line)) break; // dedented, so the block ended
|
|
145
|
-
body.push(line.trim());
|
|
146
|
-
}
|
|
147
|
-
return body.join(' ').trim();
|
|
148
|
-
}
|
|
142
|
+
function argProjectionMode(): ProjectionMode | undefined {
|
|
143
|
+
const raw = argValue('--projection-mode');
|
|
144
|
+
if (raw === null) return undefined;
|
|
145
|
+
if (raw === 'scoped' || raw === 'legacy-all') return raw;
|
|
146
|
+
throw new Error(`--projection-mode must be scoped or legacy-all, got: ${raw}`);
|
|
147
|
+
}
|
|
149
148
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return '';
|
|
149
|
+
/** `~` expanded against the current user's home. */
|
|
150
|
+
function expandHome(p: string): string {
|
|
151
|
+
return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* hypothesis is measured against.
|
|
155
|
+
* `--limits` — publish the per-host picture: what each host was measured to
|
|
156
|
+
* deliver, and whether the observed hosts truncate the same way.
|
|
159
157
|
*/
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// into `dist/agent-src/skills/`, and a Dirent for a symlink reports
|
|
164
|
-
// `isDirectory() === false` — that filter silently read 47 of 289 entries
|
|
165
|
-
// and reported the result as a complete catalogue. `existsSync` follows
|
|
166
|
-
// the link, so the scan sees what the host sees.
|
|
167
|
-
const names = fs
|
|
168
|
-
.readdirSync(root)
|
|
169
|
-
.filter((n) => fs.existsSync(path.join(root, n, 'SKILL.md')))
|
|
170
|
-
.sort();
|
|
171
|
-
|
|
172
|
-
return names.map((name, index) => {
|
|
173
|
-
const content = fs.readFileSync(path.join(root, name, 'SKILL.md'), 'utf-8');
|
|
174
|
-
const frontmatter = frontmatterOf(content);
|
|
175
|
-
const description = descriptionOf(frontmatter);
|
|
176
|
-
return {
|
|
177
|
-
name,
|
|
178
|
-
position: index + 1,
|
|
179
|
-
hasDescription: description.length > 0,
|
|
180
|
-
descriptionLength: description.length,
|
|
181
|
-
frontmatterKeys: topLevelKeys(frontmatter),
|
|
182
|
-
};
|
|
183
|
-
});
|
|
184
|
-
}
|
|
158
|
+
function runLimitsMode(): number {
|
|
159
|
+
const records = readObservationLog(path.join(REPO, OBSERVATION_LOG));
|
|
160
|
+
const limits = knownHostLimits(records);
|
|
185
161
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
162
|
+
if (process.argv.includes('--json')) {
|
|
163
|
+
process.stdout.write(
|
|
164
|
+
`${JSON.stringify({ hosts: [...limits.values()], observations: records.length }, null, 2)}\n`,
|
|
165
|
+
);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
process.stdout.write(`${formatPerHostVerdicts(records)}\n\n`);
|
|
170
|
+
if (limits.size === 0) {
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
'measured truncations: none.\nOnly a host that publishes its own dropped count yields one; a\nself-reported observation states which entries arrived bare, which is a\nselector question, not a ceiling.\n',
|
|
173
|
+
);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
process.stdout.write('measured truncations:\n');
|
|
177
|
+
for (const limit of [...limits.values()].sort((a, b) => a.host.localeCompare(b.host))) {
|
|
178
|
+
process.stdout.write(
|
|
179
|
+
` ${limit.host}: dropped ${limit.droppedEntries} entries on ${limit.observedAt}, ` +
|
|
180
|
+
`at a projection of ${limit.projectedVolume}\n`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return 0;
|
|
194
184
|
}
|
|
195
185
|
|
|
196
186
|
/**
|
|
197
|
-
*
|
|
198
|
-
* any, separates the bare entries from the described ones.
|
|
187
|
+
* `--projection-modes` — the two modes side by side, as a MEASUREMENT.
|
|
199
188
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
189
|
+
* It reads no setting, writes no setting, and recommends no default. The
|
|
190
|
+
* scoped-projection default and its migration notice are owned by a different
|
|
191
|
+
* roadmap and were decided on 2026-08-15; this mode exists so the size of the
|
|
192
|
+
* difference is a published number instead of an intuition, and it deliberately
|
|
193
|
+
* offers no flag that would change anything.
|
|
194
|
+
*
|
|
195
|
+
* `--host-root` is repeatable, so several installed hosts are read against the
|
|
196
|
+
* same pair of numbers in one run. Each host is reported on its own row: no
|
|
197
|
+
* host's count is ever used to say anything about another's.
|
|
205
198
|
*/
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const describedInput =
|
|
214
|
-
observedDescribed ?? projected.map((e) => e.name).filter((n) => !bare.includes(n));
|
|
215
|
-
const described = describedInput.filter((n) => known.has(n));
|
|
216
|
-
|
|
217
|
-
// Both self-reported lists are validated, not only the bare one: a typo in
|
|
218
|
-
// --described silently shrank the described set and weakened the analysis,
|
|
219
|
-
// while the same typo in --observed was surfaced.
|
|
220
|
-
const unknownObserved = [
|
|
221
|
-
...new Set([
|
|
222
|
-
...observedBare.filter((n) => !known.has(n)),
|
|
223
|
-
...(observedDescribed ?? []).filter((n) => !known.has(n)),
|
|
224
|
-
]),
|
|
225
|
-
].sort();
|
|
226
|
-
|
|
227
|
-
const base = {
|
|
228
|
-
entriesTotal: projected.length,
|
|
229
|
-
observedBare: [...bare].sort(),
|
|
230
|
-
observedDescribed: [...described].sort(),
|
|
231
|
-
unknownObserved,
|
|
199
|
+
function runProjectionModesMode(): number {
|
|
200
|
+
const stats = scoped_projection_stats(REPO, iter_skills());
|
|
201
|
+
const counts: ProjectionModeCounts = {
|
|
202
|
+
scoped: stats.projected,
|
|
203
|
+
legacyAll: stats.total,
|
|
204
|
+
prunedUnderScoped: stats.pruned,
|
|
205
|
+
activePacks: stats.active_packs,
|
|
232
206
|
};
|
|
233
207
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
208
|
+
const rows: HostProjectionRow[] = [];
|
|
209
|
+
for (const rootArg of argValues('--host-root')) {
|
|
210
|
+
const root = expandHome(rootArg);
|
|
211
|
+
if (!fs.existsSync(root)) {
|
|
212
|
+
process.stderr.write(`❌ host root does not exist: ${root}\n`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
const host = path.basename(root).replace(/^\./, '');
|
|
216
|
+
rows.push(classifyHostProjection(measureCatalogueVolume(host, root), counts));
|
|
240
217
|
}
|
|
241
218
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// 1. The projection itself — does the entry even declare a description?
|
|
247
|
-
const bareWithDesc = bareEntries.filter((e) => e.hasDescription).length;
|
|
248
|
-
const describedWithoutDesc = describedEntries.filter((e) => !e.hasDescription).length;
|
|
249
|
-
candidates.push({
|
|
250
|
-
id: 'declares-description',
|
|
251
|
-
describes: 'the projected SKILL.md declares a `description:`',
|
|
252
|
-
separates: bareWithDesc === 0 && describedWithoutDesc === 0,
|
|
253
|
-
detail: `${bareWithDesc}/${bareEntries.length} bare entries declare one; ${describedWithoutDesc}/${describedEntries.length} described entries do not`,
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// 2. The head-N hypothesis the census proposed — falsifiable here.
|
|
257
|
-
const barePositions = bareEntries.map((e) => e.position);
|
|
258
|
-
const describedPositions = describedEntries.map((e) => e.position);
|
|
259
|
-
const positional = Math.max(...describedPositions) < Math.min(...barePositions);
|
|
260
|
-
candidates.push({
|
|
261
|
-
id: 'positional-head',
|
|
262
|
-
describes: 'every described entry sorts before every bare entry (a head-N budget)',
|
|
263
|
-
separates: positional,
|
|
264
|
-
detail: positional
|
|
265
|
-
? `described top out at #${Math.max(...describedPositions)}, bare start at #${Math.min(...barePositions)}`
|
|
266
|
-
: `described reach #${Math.max(...describedPositions)} while bare start at #${Math.min(...barePositions)} — the ranges overlap, so no head-N cut explains this`,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// 3. Description length as a budget proxy.
|
|
270
|
-
const lengthSeparates = rangesAreDisjoint(
|
|
271
|
-
bareEntries.map((e) => e.descriptionLength),
|
|
272
|
-
describedEntries.map((e) => e.descriptionLength),
|
|
273
|
-
);
|
|
274
|
-
candidates.push({
|
|
275
|
-
id: 'description-length',
|
|
276
|
-
describes: 'description length ranges do not overlap between the groups',
|
|
277
|
-
separates: lengthSeparates,
|
|
278
|
-
detail: lengthSeparates
|
|
279
|
-
? 'a length threshold separates the two groups'
|
|
280
|
-
: 'length ranges overlap',
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// 4. Any frontmatter key whose presence tracks the split exactly.
|
|
284
|
-
const allKeys = new Set<string>();
|
|
285
|
-
for (const entry of projected) for (const key of entry.frontmatterKeys) allKeys.add(key);
|
|
286
|
-
for (const key of [...allKeys].sort()) {
|
|
287
|
-
const bareHas = bareEntries.filter((e) => e.frontmatterKeys.includes(key)).length;
|
|
288
|
-
const describedHas = describedEntries.filter((e) =>
|
|
289
|
-
e.frontmatterKeys.includes(key),
|
|
290
|
-
).length;
|
|
291
|
-
const separates =
|
|
292
|
-
(bareHas === 0 && describedHas === describedEntries.length) ||
|
|
293
|
-
(describedHas === 0 && bareHas === bareEntries.length);
|
|
294
|
-
if (!separates) continue;
|
|
295
|
-
candidates.push({
|
|
296
|
-
id: `frontmatter:${key}`,
|
|
297
|
-
describes: `presence of the \`${key}:\` frontmatter key`,
|
|
298
|
-
separates: true,
|
|
299
|
-
detail: `bare ${bareHas}/${bareEntries.length}, described ${describedHas}/${describedEntries.length}`,
|
|
300
|
-
});
|
|
219
|
+
if (process.argv.includes('--json')) {
|
|
220
|
+
process.stdout.write(`${JSON.stringify({ counts, hosts: rows }, null, 2)}\n`);
|
|
221
|
+
return 0;
|
|
301
222
|
}
|
|
302
|
-
|
|
303
|
-
return
|
|
304
|
-
...base,
|
|
305
|
-
candidates,
|
|
306
|
-
verdict: candidates.some((c) => c.separates)
|
|
307
|
-
? ('selector-found' as const)
|
|
308
|
-
: ('no-selector' as const),
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/** One append-only observation record. No field can hold free-form content. */
|
|
313
|
-
export interface ObservationRecord {
|
|
314
|
-
schema: 1;
|
|
315
|
-
observed_at: string;
|
|
316
|
-
host: string;
|
|
317
|
-
entries_total: number;
|
|
318
|
-
bare_count: number;
|
|
319
|
-
described_count: number;
|
|
320
|
-
bare_names: string[];
|
|
321
|
-
verdict: SelectorReport['verdict'];
|
|
322
|
-
separating_candidates: string[];
|
|
223
|
+
process.stdout.write(`${formatProjectionModes(counts, rows)}\n`);
|
|
224
|
+
return 0;
|
|
323
225
|
}
|
|
324
226
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
227
|
+
/** `--volume <host-root>` — the projection half for one host, byte-accurate. */
|
|
228
|
+
function runVolumeMode(rootArg: string): number {
|
|
229
|
+
const root = expandHome(rootArg);
|
|
230
|
+
if (!fs.existsSync(root)) {
|
|
231
|
+
process.stderr.write(`❌ host root does not exist: ${root}\n`);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
const volume = measureCatalogueVolume(argValue('--host') ?? path.basename(root).replace(/^\./, ''), root);
|
|
235
|
+
if (process.argv.includes('--json')) {
|
|
236
|
+
process.stdout.write(`${JSON.stringify(volume, null, 2)}\n`);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
process.stdout.write(
|
|
240
|
+
`host: ${volume.host}\nroot: ${volume.root}\n` +
|
|
241
|
+
`skill entries: ${volume.skillEntries}\ncommand entries: ${volume.commandEntries}\n` +
|
|
242
|
+
`artefacts offered: ${volume.artefacts}\n` +
|
|
243
|
+
`description payload: ${volume.descriptionBytes} bytes\n\n` +
|
|
244
|
+
'This is the PROJECTION half — what the host is offered, not what it delivered.\n',
|
|
245
|
+
);
|
|
246
|
+
return 0;
|
|
341
247
|
}
|
|
342
248
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
249
|
+
/**
|
|
250
|
+
* `--host-event <file|->` — record what the host itself published.
|
|
251
|
+
*
|
|
252
|
+
* The deterministic path. `-` reads stdin, so the capture composes directly:
|
|
253
|
+
* codex exec --json --skip-git-repo-check "reply with exactly: OK" \
|
|
254
|
+
* | capture_skill_catalogue --host-event - --host codex --host-root ~/.codex
|
|
255
|
+
*/
|
|
256
|
+
function runHostEventMode(source: string): number {
|
|
257
|
+
const stream =
|
|
258
|
+
source === '-'
|
|
259
|
+
? fs.readFileSync(0, 'utf-8')
|
|
260
|
+
: fs.readFileSync(expandHome(source), 'utf-8');
|
|
261
|
+
|
|
262
|
+
const hostRootArg = argValue('--host-root');
|
|
263
|
+
if (!hostRootArg) {
|
|
264
|
+
process.stderr.write(
|
|
265
|
+
'❌ --host-event requires --host-root <dir> — the dropped count is only\n' +
|
|
266
|
+
' meaningful against the number of artefacts the host was offered.\n',
|
|
359
267
|
);
|
|
360
|
-
return
|
|
268
|
+
return 1;
|
|
361
269
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
270
|
+
const host = argValue('--host') ?? 'unknown';
|
|
271
|
+
const volume = measureCatalogueVolume(host, expandHome(hostRootArg));
|
|
272
|
+
if (volume.artefacts === 0) {
|
|
273
|
+
process.stderr.write(`❌ host root ${volume.root} offers no catalogue artefacts\n`);
|
|
274
|
+
return 1;
|
|
367
275
|
}
|
|
368
|
-
lines.push('');
|
|
369
|
-
lines.push(
|
|
370
|
-
report.verdict === 'selector-found'
|
|
371
|
-
? 'verdict: selector-found — a delivery fix can act on the separating property above.'
|
|
372
|
-
: 'verdict: no-selector — no measured property separates the groups; the selector is host-internal on this evidence. Publish the null rather than guessing a fix.',
|
|
373
|
-
);
|
|
374
|
-
return lines.join('\n');
|
|
375
|
-
}
|
|
376
276
|
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
if (!
|
|
380
|
-
|
|
277
|
+
const event = parseHostBudgetEvent(stream);
|
|
278
|
+
if (event === null) {
|
|
279
|
+
if (!process.argv.includes('--assert-no-truncation')) {
|
|
280
|
+
process.stderr.write(
|
|
281
|
+
'❌ no parseable skills-budget event in this stream.\n' +
|
|
282
|
+
' That is UNKNOWN, not zero: a reworded or removed host message looks\n' +
|
|
283
|
+
' exactly like a fixed defect. If this run genuinely did not truncate,\n' +
|
|
284
|
+
' say so with --assert-no-truncation.\n',
|
|
285
|
+
);
|
|
286
|
+
return 1;
|
|
381
287
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
288
|
+
process.stdout.write(
|
|
289
|
+
`host: ${host}\noffered: ${volume.artefacts} (${volume.skillEntries} skills + ${volume.commandEntries} commands)\n` +
|
|
290
|
+
`dropped: 0 (asserted — no budget event in the stream)\n` +
|
|
291
|
+
'descriptions stripped: no — the host published nothing to strip.\n',
|
|
292
|
+
);
|
|
293
|
+
if (process.argv.includes('--record')) {
|
|
294
|
+
const stampedAt = argValue('--observed-at');
|
|
295
|
+
if (!stampedAt) {
|
|
296
|
+
process.stderr.write('❌ --record requires --observed-at <ISO date>\n');
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
const logPath = path.join(REPO, OBSERVATION_LOG);
|
|
300
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
301
|
+
fs.appendFileSync(
|
|
302
|
+
logPath,
|
|
303
|
+
`${JSON.stringify(
|
|
304
|
+
buildNoTruncationRecord(
|
|
305
|
+
host,
|
|
306
|
+
stampedAt,
|
|
307
|
+
volume.artefacts,
|
|
308
|
+
volume.skillEntries,
|
|
309
|
+
argProjectionMode(),
|
|
310
|
+
),
|
|
311
|
+
)}\n`,
|
|
312
|
+
);
|
|
313
|
+
process.stdout.write(`recorded → ${OBSERVATION_LOG}\n`);
|
|
314
|
+
}
|
|
315
|
+
return 0;
|
|
387
316
|
}
|
|
388
|
-
|
|
389
|
-
|
|
317
|
+
|
|
318
|
+
process.stdout.write(
|
|
319
|
+
`host: ${host}\nroot: ${volume.root}\n` +
|
|
320
|
+
`artefacts offered: ${volume.artefacts} (${volume.skillEntries} skills + ${volume.commandEntries} commands)\n` +
|
|
321
|
+
`description payload: ${volume.descriptionBytes} bytes\n` +
|
|
322
|
+
`dropped by the host: ${event.droppedCount}\n` +
|
|
323
|
+
`descriptions stripped: ${event.descriptionsStripped ? 'yes — all of them' : 'no'}\n\n` +
|
|
324
|
+
'Source: host-event (read from the host\'s own JSON channel, not self-reported).\n' +
|
|
325
|
+
'No delivered count is printed: the offered figure above is THIS tool\'s\n' +
|
|
326
|
+
'projection and the dropped figure is the host\'s, and a controlled probe\n' +
|
|
327
|
+
'showed they do not share a denominator. Subtracting them would invent a number.\n',
|
|
390
328
|
);
|
|
391
|
-
}
|
|
392
329
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
330
|
+
if (process.argv.includes('--record')) {
|
|
331
|
+
const stampedAt = argValue('--observed-at');
|
|
332
|
+
if (!stampedAt) {
|
|
333
|
+
process.stderr.write('❌ --record requires --observed-at <ISO date>\n');
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
const logPath = path.join(REPO, OBSERVATION_LOG);
|
|
337
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
338
|
+
const record = buildHostEventRecord(
|
|
339
|
+
host,
|
|
340
|
+
stampedAt,
|
|
341
|
+
volume.artefacts,
|
|
342
|
+
event,
|
|
343
|
+
volume.skillEntries,
|
|
344
|
+
argProjectionMode(),
|
|
345
|
+
);
|
|
346
|
+
fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`);
|
|
347
|
+
process.stdout.write(`recorded → ${OBSERVATION_LOG}\n`);
|
|
348
|
+
}
|
|
349
|
+
return 0;
|
|
397
350
|
}
|
|
398
351
|
|
|
399
352
|
function main(): number {
|
|
353
|
+
if (process.argv.includes('--limits')) return runLimitsMode();
|
|
354
|
+
if (process.argv.includes('--projection-modes')) return runProjectionModesMode();
|
|
355
|
+
|
|
356
|
+
const volumeRoot = argValue('--volume');
|
|
357
|
+
if (volumeRoot) return runVolumeMode(volumeRoot);
|
|
358
|
+
|
|
359
|
+
const hostEvent = argValue('--host-event');
|
|
360
|
+
if (hostEvent) return runHostEventMode(hostEvent);
|
|
361
|
+
|
|
400
362
|
const root = resolveCatalogueRoot(argValue('--catalogue-root'));
|
|
401
363
|
const projected = readProjectedCatalogue(root);
|
|
402
364
|
|