@event4u/agent-config 14.21.0 → 14.23.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 +164 -187
- package/CREDITS.md +37 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/commands/pr/merge.md +47 -38
- package/dist/agent-src/commands/roadmap/next.md +8 -0
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +45 -13
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +4 -14
- 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/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +13 -13
- 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 +1813 -946
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +21 -3
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +10870 -7641
- package/dist/scripts/_lib/lean_projection_mode.js +108 -0
- package/dist/scripts/_lib/router_match.js +243 -0
- package/dist/scripts/_lib/rule_injection.js +216 -0
- package/dist/scripts/condense.js +52 -45
- package/dist/scripts/project_thin_rules.js +139 -19
- package/dist/server/schemas/settings.js +16 -3
- package/docs/MIGRATION.md +105 -1
- package/docs/contracts/settings-classes.md +40 -8
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
- package/llms.txt +1 -1
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/agent-settings.template.yml +47 -1
- package/src/config/continuity-surface.json +516 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/hook-token-budget.json +7 -6
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/config/routing-coverage-seed.json +9 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
- package/src/scripts/_cli/handoff_sessions.ts +14 -44
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_slot.ts +295 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/continuity_writer.ts +253 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +86 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/lean_projection_mode.ts +127 -0
- package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
- package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/session_eol.ts +44 -0
- package/src/scripts/_lib/session_index_trust.ts +346 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/audit_mcp_tools.ts +69 -25
- package/src/scripts/build_mcp_catalog.ts +24 -1
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_host_tree_parity.ts +291 -0
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_preamble_payload_budget.ts +136 -7
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_rule_projection_integrity.ts +102 -2
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/ci_settle.ts +205 -5
- package/src/scripts/code_graph/build.ts +217 -15
- package/src/scripts/code_graph/cli.ts +334 -15
- package/src/scripts/code_graph/detect.ts +38 -0
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +144 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +262 -6
- package/src/scripts/code_graph/types.ts +117 -3
- package/src/scripts/code_graph/validate.ts +26 -1
- package/src/scripts/code_graph/verbs.ts +590 -0
- package/src/scripts/condense.ts +54 -42
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +118 -1
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +87 -12
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/push_settle_hook.ts +3 -2
- package/src/scripts/hooks/rule_inject_hook.ts +108 -21
- package/src/scripts/hooks/session_eol_hook.ts +120 -2
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/hot_context_hook.ts +62 -7
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/lint_roadmap_blockers.ts +164 -1
- package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
- package/src/scripts/mcp_server/graph_tools.ts +294 -0
- package/src/scripts/mcp_server/path_util.ts +31 -0
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
- package/src/scripts/mcp_server/tools.ts +5 -69
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/model_rule_injection.ts +61 -4
- package/src/scripts/project_thin_rules.ts +149 -16
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/report_standing_payload_by_host.ts +650 -0
- package/src/scripts/schemas/agent-settings.schema.json +7 -1
- package/src/scripts/session_memory_index.ts +81 -8
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/server/schemas/settings.ts +23 -4
- package/src/vendor/grammars/README.md +86 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* The per-host injection-effect census — one line per host, provenance or none.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-delivery-on-hook-hosts` steps 1.2 and 1.3. It renders the committed
|
|
6
|
+
* record at `src/config/host-injection-effect.json` and adds the one thing the
|
|
7
|
+
* record deliberately does not hold: the COMPUTED half, read fresh from the hook
|
|
8
|
+
* manifest, so a reader sees both axes side by side and can see that they
|
|
9
|
+
* disagree.
|
|
10
|
+
*
|
|
11
|
+
* WHY BOTH COLUMNS ARE PRINTED, ALWAYS
|
|
12
|
+
* ------------------------------------
|
|
13
|
+
* `cowork` is the row that makes this table worth generating: eight slots bound
|
|
14
|
+
* and a measured `observed-false`. Printing only the observation would hide the
|
|
15
|
+
* near-miss; printing only the binding would produce the wrong verdict for the
|
|
16
|
+
* one host we have actually measured. The table exists so the gap between "could
|
|
17
|
+
* emit" and "did arrive" is visible per host rather than argued in prose.
|
|
18
|
+
*
|
|
19
|
+
* It computes NO verdict. `admissibleUnderE3` is a one-line read of the observed
|
|
20
|
+
* column and is applied by the caller that would change a projection, never here:
|
|
21
|
+
* a report that also decided admission would be the single place where an
|
|
22
|
+
* inference could quietly replace an observation.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ./scripts-run src/scripts/report_host_injection_effect
|
|
26
|
+
* ./scripts-run src/scripts/report_host_injection_effect --emit --pin <sha>
|
|
27
|
+
*
|
|
28
|
+
* Deterministic: hosts sort by id, no timestamp, the pin is an argument.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
concernBound,
|
|
38
|
+
loadRecord,
|
|
39
|
+
recordProblems,
|
|
40
|
+
type InjectionRecord,
|
|
41
|
+
} from './_lib/injection_effect.js';
|
|
42
|
+
|
|
43
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
44
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
45
|
+
export const ARTIFACT_REL = path.join(
|
|
46
|
+
'agents', 'evidence', 'analysis', 'host-injection-effect-2026-09.md',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/** Platform → slot → concern ids, straight out of the manifest. */
|
|
50
|
+
export function loadPlatforms(root: string): Record<string, Record<string, string[]>> {
|
|
51
|
+
const raw = fs.readFileSync(path.join(root, 'src', 'scripts', 'hook_manifest.yaml'), 'utf-8');
|
|
52
|
+
const out = spawnSync(
|
|
53
|
+
process.execPath,
|
|
54
|
+
[
|
|
55
|
+
'-e',
|
|
56
|
+
'const y=require("js-yaml");let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{' +
|
|
57
|
+
'const d=y.load(s);process.stdout.write(JSON.stringify(d.platforms||{}))})',
|
|
58
|
+
],
|
|
59
|
+
{ input: raw, encoding: 'utf-8', cwd: root, maxBuffer: 64 * 1024 * 1024 },
|
|
60
|
+
);
|
|
61
|
+
if (out.status !== 0 || !out.stdout) {
|
|
62
|
+
throw new Error(`hook_manifest.yaml could not be parsed: ${out.stderr || 'no output'}`);
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(out.stdout) as Record<string, Record<string, string[]>>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Keys under a platform that are NOT lifecycle slots.
|
|
69
|
+
*
|
|
70
|
+
* `fallback_only` is a marker saying this package binds nothing here, and
|
|
71
|
+
* counting it as a slot made copilot read 1 against the 0 that
|
|
72
|
+
* `docs/enforcement-by-host.md` records — a one-row disagreement with the host
|
|
73
|
+
* table for a nameable reason, which is the drift this repository gates
|
|
74
|
+
* elsewhere. Caught by comparing the generated table against that doc rather
|
|
75
|
+
* than by a test, so it is named here.
|
|
76
|
+
*/
|
|
77
|
+
const NON_SLOT_KEYS: ReadonlySet<string> = new Set(['fallback_only']);
|
|
78
|
+
|
|
79
|
+
/** Lifecycle slots bound for a host, on the host table's own definition. */
|
|
80
|
+
export function boundSlotCount(platforms: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>, host: string): number {
|
|
81
|
+
return Object.keys(platforms[host] ?? {}).filter((k) => !NON_SLOT_KEYS.has(k)).length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Host ids that are real rows, in sort order. Underscore keys are annotations. */
|
|
85
|
+
export function hostIds(rec: InjectionRecord): string[] {
|
|
86
|
+
return Object.keys(rec).filter((k) => !k.startsWith('_')).sort();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function renderArtifact(root: string, pin: string): string {
|
|
90
|
+
const rec = loadRecord(root);
|
|
91
|
+
const platforms = loadPlatforms(root);
|
|
92
|
+
const L: string[] = [];
|
|
93
|
+
L.push('<!-- evidence-type: analysis -->');
|
|
94
|
+
L.push('# Host injection effect — does a delivered rule body reach the model?');
|
|
95
|
+
L.push('');
|
|
96
|
+
L.push(`Pinned to commit \`${pin}\`. Generated by`);
|
|
97
|
+
L.push(`\`./scripts-run src/scripts/report_host_injection_effect --emit --pin ${pin}\`.`);
|
|
98
|
+
L.push('Observations come from `src/config/host-injection-effect.json`; the binding column is');
|
|
99
|
+
L.push('read fresh from `src/scripts/hook_manifest.yaml` on every run.');
|
|
100
|
+
L.push('');
|
|
101
|
+
L.push('## The two columns, and why neither is the other');
|
|
102
|
+
L.push('');
|
|
103
|
+
L.push('**Slots bound** is computed and cheap. **Body reaches the model** is observed and');
|
|
104
|
+
L.push('expensive, and owner ruling E3 admits a host to `lean_projection.hosts` on the second');
|
|
105
|
+
L.push('only: one transcript in which a model visibly acts on a delivered body. Documentation');
|
|
106
|
+
L.push('of a host\'s hook API is not admission.');
|
|
107
|
+
L.push('');
|
|
108
|
+
L.push('`cowork` is why the distinction is not pedantry: **eight slots bound, and measured');
|
|
109
|
+
L.push('`observed-false`** — its trampoline discards dispatcher output and exits 0. A table');
|
|
110
|
+
L.push('with only the binding column would admit it.');
|
|
111
|
+
L.push('');
|
|
112
|
+
L.push('## Per host');
|
|
113
|
+
L.push('');
|
|
114
|
+
L.push('| Host | Slots bound | `rule-inject` on `user_prompt_submit` | Body reaches the model | Citation |');
|
|
115
|
+
L.push('|---|---:|:-:|---|---|');
|
|
116
|
+
for (const host of hostIds(rec)) {
|
|
117
|
+
const obs = rec[host];
|
|
118
|
+
const slots = boundSlotCount(platforms, host);
|
|
119
|
+
const bound = concernBound(platforms, host, 'user_prompt_submit', 'rule-inject');
|
|
120
|
+
const cite =
|
|
121
|
+
obs !== undefined && obs.transcript !== null && obs.date !== null
|
|
122
|
+
? `\`${obs.transcript}\` (${obs.date})`
|
|
123
|
+
: '—';
|
|
124
|
+
L.push(
|
|
125
|
+
`| \`${host}\` | ${String(slots)} | ${bound ? 'yes' : 'no'} | \`${obs?.state ?? 'unobserved'}\` | ${cite} |`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
L.push('');
|
|
129
|
+
L.push('## Why each row reads as it does');
|
|
130
|
+
L.push('');
|
|
131
|
+
for (const host of hostIds(rec)) {
|
|
132
|
+
L.push(`- **\`${host}\`** — ${rec[host]?.reason ?? 'no row'}`);
|
|
133
|
+
}
|
|
134
|
+
const mdTree = rec['_cursor_md_tree_loaded'];
|
|
135
|
+
if (mdTree !== undefined) {
|
|
136
|
+
L.push('');
|
|
137
|
+
L.push('## `cursor.md_tree_loaded` (step 1.3)');
|
|
138
|
+
L.push('');
|
|
139
|
+
L.push(`\`cursor.md_tree_loaded: ${mdTree.state}\` — ${mdTree.reason}`);
|
|
140
|
+
}
|
|
141
|
+
L.push('');
|
|
142
|
+
L.push('## What this census does NOT do');
|
|
143
|
+
L.push('');
|
|
144
|
+
L.push('It computes no verdict. Admission under E3 is a one-line read of the observed column');
|
|
145
|
+
L.push('and is applied by whatever would change a projection, never here — a report that also');
|
|
146
|
+
L.push('decided admission would be the one place an inference could quietly replace an');
|
|
147
|
+
L.push('observation.');
|
|
148
|
+
L.push('');
|
|
149
|
+
L.push('`unobserved` is a RESULT, not a gap: the host keeps its full rule bodies and loses');
|
|
150
|
+
L.push('nothing while it waits, which is the roadmap\'s K6 and is asserted per PR by');
|
|
151
|
+
L.push('`check_host_tree_parity` in the predecessor.');
|
|
152
|
+
L.push('');
|
|
153
|
+
return L.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function headSha(root: string): string {
|
|
157
|
+
return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function main(argv: readonly string[]): number {
|
|
161
|
+
const root = REPO_ROOT;
|
|
162
|
+
const rec = loadRecord(root);
|
|
163
|
+
const problems = recordProblems(rec);
|
|
164
|
+
if (problems.length > 0) {
|
|
165
|
+
process.stderr.write('❌ the observation record is not admissible:\n');
|
|
166
|
+
for (const p of problems) process.stderr.write(` · ${p.host}: ${p.message}\n`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
const pinIdx = argv.indexOf('--pin');
|
|
170
|
+
const pin = pinIdx !== -1 && pinIdx + 1 < argv.length ? (argv[pinIdx + 1] as string) : headSha(root);
|
|
171
|
+
if (argv.includes('--emit')) {
|
|
172
|
+
const out = path.join(root, ARTIFACT_REL);
|
|
173
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
174
|
+
fs.writeFileSync(out, renderArtifact(root, pin), 'utf-8');
|
|
175
|
+
process.stdout.write(`wrote ${ARTIFACT_REL} · pin ${pin}\n`);
|
|
176
|
+
}
|
|
177
|
+
const hosts = hostIds(rec);
|
|
178
|
+
const byState = new Map<string, number>();
|
|
179
|
+
for (const h of hosts) byState.set(rec[h]!.state, (byState.get(rec[h]!.state) ?? 0) + 1);
|
|
180
|
+
process.stdout.write(`scanned: ${String(hosts.length)} host(s)\n`);
|
|
181
|
+
for (const [state, n] of [...byState].sort()) {
|
|
182
|
+
process.stdout.write(` ${state.padEnd(16)} ${String(n)}\n`);
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
188
|
+
process.exit(main(process.argv.slice(2)));
|
|
189
|
+
}
|
|
@@ -24,6 +24,7 @@ import * as path from 'node:path';
|
|
|
24
24
|
import process from 'node:process';
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
26
|
|
|
27
|
+
import { resolveHostLayerVerdict, setPartitionAnnounce } from '../install/partitionEligibility.js';
|
|
27
28
|
import { warnLayerOverlap } from './_lib/layer_overlap_notice.js';
|
|
28
29
|
|
|
29
30
|
// ledger-exempt: this is a REPORTER, not a gate — it always exits 0 by construction
|
|
@@ -59,6 +60,46 @@ export function main(argv?: readonly string[]): number {
|
|
|
59
60
|
return 1;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
// The projection line, and where it lives is a correction. **Both council seats
|
|
64
|
+
// (2026-08-20, 2/2) required that generation PRINT the mode it selected** rather
|
|
65
|
+
// than partition silently. The 2026-09-07 change moved the withhold off
|
|
66
|
+
// `installed.lock` and, in doing so, removed `condense.ts`'s only call into the
|
|
67
|
+
// resolver — so the emitter it installed became dead and generation withheld
|
|
68
|
+
// ~299 skills and 29 personas while printing nothing about the layer it
|
|
69
|
+
// withheld against. A neutral review found it the same day.
|
|
70
|
+
//
|
|
71
|
+
// It prints from HERE rather than from `condense.ts` because that file is ~1,200
|
|
72
|
+
// lines past the source-size ceiling, where the ratchet counts every added line
|
|
73
|
+
// and its test blocks on `baseline == live`. This step already runs in the same
|
|
74
|
+
// `generate-tools` chain, one line below the generator, so the operator sees the
|
|
75
|
+
// two lines together.
|
|
76
|
+
// Silence the resolver's OWN one-liner first: it writes to stdout by default,
|
|
77
|
+
// and leaving it on printed the verdict twice — once bare, once with the
|
|
78
|
+
// remediation text below. One line, the useful one.
|
|
79
|
+
setPartitionAnnounce(() => undefined);
|
|
80
|
+
const verdict = resolveHostLayerVerdict(projectRoot);
|
|
81
|
+
// Three states, not two. Corrected after a second neutral review: the
|
|
82
|
+
// unverified branch printed "a stale global layer is fixed by
|
|
83
|
+
// `agent-config install`" on a machine with NO global layer at all — a fresh
|
|
84
|
+
// checkout, where nothing is withheld, nothing is stale, and the remediation
|
|
85
|
+
// named does not apply. It fired on every `task generate-tools` there.
|
|
86
|
+
//
|
|
87
|
+
// What this line reports is the VERIFICATION state of the layer withheld
|
|
88
|
+
// against. The per-artefact withhold COUNTS are the generator's own summary
|
|
89
|
+
// one line above (`skills=N`, `command_skills=N (M withheld …)`), which is
|
|
90
|
+
// where a reader sees what was actually held back.
|
|
91
|
+
const noLayer = verdict.reason.includes('no host-global layer');
|
|
92
|
+
process.stdout.write(
|
|
93
|
+
verdict.verified
|
|
94
|
+
? ` ℹ️ project layer carries only what ~/.claude lacks — ${verdict.reason}\n`
|
|
95
|
+
: noLayer
|
|
96
|
+
? ' ℹ️ no host-global layer on this machine — nothing is withheld, so the\n' +
|
|
97
|
+
' project layer carries the full projection by construction.\n'
|
|
98
|
+
: ` ⚠️ host layer UNVERIFIED — ${verdict.reason}\n` +
|
|
99
|
+
' The project layer still carries only what ~/.claude lacks (per-artefact,\n' +
|
|
100
|
+
' ADR-236 amendment 2026-09-07). A stale global layer is fixed by\n' +
|
|
101
|
+
' `agent-config install`, not by re-running generate-tools.\n',
|
|
102
|
+
);
|
|
62
103
|
warnLayerOverlap(projectRoot, (m) => process.stdout.write(`${m}\n`));
|
|
63
104
|
return 0;
|
|
64
105
|
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Every shipped skill, and the entry paths that can reach it.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-skill-menu-economy` step 1.1. The roadmap's lever is COUNT ON THE
|
|
6
|
+
* MENU — the description corpus already sits at the schema cap, so there is no
|
|
7
|
+
* byte-shaving budget to spend (K2). Deciding which skills may leave the model
|
|
8
|
+
* menu needs to know, per skill, whether anything other than the menu can
|
|
9
|
+
* reach it. This report answers that mechanically and publishes the rule it
|
|
10
|
+
* used, so a later reader can disagree with the rule rather than with a number.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IT IS NOT
|
|
13
|
+
* --------------
|
|
14
|
+
* It is a REPORT, deliberately: no threshold, no exit code that carries a
|
|
15
|
+
* verdict, and a `report_` prefix so `_lib/gate_population.ts` classifies it
|
|
16
|
+
* out of the gate population rather than by whoever last edited a manifest.
|
|
17
|
+
*
|
|
18
|
+
* It also does NOT decide "sole entry path", and that limit is load-bearing
|
|
19
|
+
* rather than an apology. A command reference proves a command CAN reach the
|
|
20
|
+
* skill; it cannot prove the menu never does. The roadmap's own Risk 1 is
|
|
21
|
+
* precisely this — marking a skill `user-invocable: false` is correct only if
|
|
22
|
+
* the command or flow path really is the only one — so this report produces
|
|
23
|
+
* the evidence for that judgement and never the judgement.
|
|
24
|
+
*
|
|
25
|
+
* THE CLASSIFICATION RULE, stated because it is the whole content
|
|
26
|
+
* -------------------------------------------------------------
|
|
27
|
+
* For each `src/skills/<name>/SKILL.md`, three signals are read:
|
|
28
|
+
*
|
|
29
|
+
* command_refs files under `src/domains/**\/command.md` that reference the
|
|
30
|
+
* skill in one of three DISAMBIGUATING shapes — `skill:<name>`,
|
|
31
|
+
* a `skills/<name>` path, or the bare name in backticks. A
|
|
32
|
+
* plain word-boundary match is deliberately NOT used: skill
|
|
33
|
+
* names like `security`, `database` and `docker` are ordinary
|
|
34
|
+
* English and would hit on every prose mention.
|
|
35
|
+
* flow_refs files under `src/flows/` whose own `skills:` list names it.
|
|
36
|
+
* Flows carry YAML name lists rather than the shapes above, so
|
|
37
|
+
* the command regex read 0 for all 299 on the first pass — a
|
|
38
|
+
* false zero, and the reason `flowSkillNames` exists.
|
|
39
|
+
* menu_present true unless the frontmatter carries `user-invocable: false`
|
|
40
|
+
* or `disable-model-invocation: true` — the two menu-economy
|
|
41
|
+
* keys the roadmap measures as nearly unused.
|
|
42
|
+
*
|
|
43
|
+
* and the row is classified:
|
|
44
|
+
*
|
|
45
|
+
* both command_refs > 0 AND flow_refs > 0
|
|
46
|
+
* command-only command_refs > 0, flow_refs = 0
|
|
47
|
+
* flow-only flow_refs > 0, command_refs = 0
|
|
48
|
+
* model-routed neither, and the skill is on the menu — the menu is then the
|
|
49
|
+
* only entry path this repository can point at
|
|
50
|
+
* orphan neither, and the skill is off the menu: nothing in the tree
|
|
51
|
+
* reaches it. Reported, never deleted (K3).
|
|
52
|
+
*
|
|
53
|
+
* `command-only` and `flow-only` name a CANDIDATE for `user-invocable: false`,
|
|
54
|
+
* never a decision — see the sole-entry limit above.
|
|
55
|
+
*
|
|
56
|
+
* Usage:
|
|
57
|
+
* ./scripts-run src/scripts/report_skill_menu_census # stdout summary
|
|
58
|
+
* ./scripts-run src/scripts/report_skill_menu_census --emit # write the artifact
|
|
59
|
+
* ./scripts-run src/scripts/report_skill_menu_census --emit --pin <sha>
|
|
60
|
+
*
|
|
61
|
+
* Deterministic: rows sort by skill name, no timestamp is written, and the pin
|
|
62
|
+
* is an argument rather than "now", so a re-run at the same pin over the same
|
|
63
|
+
* tree is byte-identical.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
import { execFileSync } from 'node:child_process';
|
|
67
|
+
import * as fs from 'node:fs';
|
|
68
|
+
import * as path from 'node:path';
|
|
69
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
70
|
+
|
|
71
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
72
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
73
|
+
export const ARTIFACT_REL = path.join('agents', 'evidence', 'analysis', 'skill-menu-census-2026-09.md');
|
|
74
|
+
|
|
75
|
+
export type EntryPath = 'both' | 'command-only' | 'flow-only' | 'model-routed' | 'orphan';
|
|
76
|
+
|
|
77
|
+
export interface SkillRow {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly commandRefs: number;
|
|
80
|
+
readonly flowRefs: number;
|
|
81
|
+
readonly menuPresent: boolean;
|
|
82
|
+
readonly hasTriggerCorpus: boolean;
|
|
83
|
+
readonly entryPath: EntryPath;
|
|
84
|
+
/** One command or flow file, for an operator to start from. Empty when none. */
|
|
85
|
+
readonly firstRef: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Escape a skill name for use inside a RegExp. Names are kebab-case, so this is cheap insurance. */
|
|
89
|
+
function escapeRe(s: string): string {
|
|
90
|
+
return s.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The three disambiguating reference shapes, as one regex.
|
|
95
|
+
*
|
|
96
|
+
* A bare word-boundary match was tried first and rejected on inspection: 299
|
|
97
|
+
* skill names include `security`, `database`, `docker`, `performance` and
|
|
98
|
+
* `mode`, each of which appears in ordinary command prose. A reference that
|
|
99
|
+
* cannot be told from a sentence is not evidence of an entry path.
|
|
100
|
+
*/
|
|
101
|
+
export function referenceRe(skill: string): RegExp {
|
|
102
|
+
const n = escapeRe(skill);
|
|
103
|
+
const tick = String.fromCharCode(96);
|
|
104
|
+
return new RegExp(
|
|
105
|
+
'skill:\\s*' + n + '(?![a-z0-9-])' +
|
|
106
|
+
'|skills/' + n + '(?:[/\\s' + tick + ')\\]]|$)' +
|
|
107
|
+
'|' + tick + n + tick,
|
|
108
|
+
'u',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Every file under `dir` matching `pred`, sorted, as repo-relative paths. */
|
|
113
|
+
export function walk(root: string, dir: string, pred: (rel: string) => boolean): string[] {
|
|
114
|
+
const out: string[] = [];
|
|
115
|
+
const abs = path.join(root, dir);
|
|
116
|
+
if (!fs.existsSync(abs)) return out;
|
|
117
|
+
const stack: string[] = [dir];
|
|
118
|
+
while (stack.length > 0) {
|
|
119
|
+
const cur = stack.pop() as string;
|
|
120
|
+
let entries: fs.Dirent[];
|
|
121
|
+
try {
|
|
122
|
+
entries = fs.readdirSync(path.join(root, cur), { withFileTypes: true });
|
|
123
|
+
} catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const e of entries) {
|
|
127
|
+
const rel = path.join(cur, e.name);
|
|
128
|
+
if (e.isDirectory()) stack.push(rel);
|
|
129
|
+
else if (e.isFile() && pred(rel)) out.push(rel);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out.sort();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Frontmatter values this report needs. Hand-parsed: only two boolean keys matter. */
|
|
136
|
+
export function menuFlags(text: string): { userInvocable: boolean | null; disableModel: boolean | null } {
|
|
137
|
+
const head = text.slice(0, 4000);
|
|
138
|
+
const ui = /^user-invocable:\s*(true|false)\s*$/mu.exec(head);
|
|
139
|
+
const dm = /^disable-model-invocation:\s*(true|false)\s*$/mu.exec(head);
|
|
140
|
+
return {
|
|
141
|
+
userInvocable: ui === null ? null : ui[1] === 'true',
|
|
142
|
+
disableModel: dm === null ? null : dm[1] === 'true',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The skill names a flow file declares, from its own `skills:` lists.
|
|
148
|
+
*
|
|
149
|
+
* Flows do NOT use the three reference shapes commands use — they carry YAML
|
|
150
|
+
* lists of bare names (`skills: [code-review, adversarial-review]`, or a block
|
|
151
|
+
* sequence). Running the command-shaped regex over them returned flow_refs = 0
|
|
152
|
+
* for all 299 skills on the first pass, which was a false zero and is why this
|
|
153
|
+
* parser exists rather than a wider word-boundary match: a bare name is safe
|
|
154
|
+
* HERE precisely because the `skills:` key bounds where it may be read.
|
|
155
|
+
*/
|
|
156
|
+
export function flowSkillNames(text: string): Set<string> {
|
|
157
|
+
const out = new Set<string>();
|
|
158
|
+
const lines = text.split('\n');
|
|
159
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
160
|
+
const line = lines[i] as string;
|
|
161
|
+
const inline = /^\s*skills:\s*\[([^\]]*)\]\s*$/u.exec(line);
|
|
162
|
+
if (inline !== null) {
|
|
163
|
+
for (const raw of (inline[1] as string).split(',')) {
|
|
164
|
+
const n = raw.trim().replace(/^['"]|['"]$/gu, '');
|
|
165
|
+
if (n !== '') out.add(n);
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!/^\s*skills:\s*$/u.test(line)) continue;
|
|
170
|
+
const indent = (/^\s*/u.exec(line) as RegExpExecArray)[0].length;
|
|
171
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
172
|
+
const item = /^(\s*)-\s*(\S.*?)\s*$/u.exec(lines[j] as string);
|
|
173
|
+
if (item === null || (item[1] as string).length <= indent) break;
|
|
174
|
+
out.add((item[2] as string).replace(/^['"]|['"]$/gu, ''));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function classify(commandRefs: number, flowRefs: number, menuPresent: boolean): EntryPath {
|
|
181
|
+
if (commandRefs > 0 && flowRefs > 0) return 'both';
|
|
182
|
+
if (commandRefs > 0) return 'command-only';
|
|
183
|
+
if (flowRefs > 0) return 'flow-only';
|
|
184
|
+
return menuPresent ? 'model-routed' : 'orphan';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function census(root: string): SkillRow[] {
|
|
188
|
+
const skillsDir = path.join(root, 'src', 'skills');
|
|
189
|
+
const names = fs
|
|
190
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
191
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))
|
|
192
|
+
.map((e) => e.name)
|
|
193
|
+
.sort();
|
|
194
|
+
|
|
195
|
+
const commandFiles = walk(root, path.join('src', 'domains'), (rel) => path.basename(rel) === 'command.md');
|
|
196
|
+
const flowFiles = walk(root, path.join('src', 'flows'), () => true);
|
|
197
|
+
const commandText = commandFiles.map((rel) => [rel, fs.readFileSync(path.join(root, rel), 'utf-8')] as const);
|
|
198
|
+
const flowDecl = flowFiles.map(
|
|
199
|
+
(rel) => [rel, flowSkillNames(fs.readFileSync(path.join(root, rel), 'utf-8'))] as const,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const rows: SkillRow[] = [];
|
|
203
|
+
for (const name of names) {
|
|
204
|
+
const skillMd = path.join(skillsDir, name, 'SKILL.md');
|
|
205
|
+
const text = fs.readFileSync(skillMd, 'utf-8');
|
|
206
|
+
const flags = menuFlags(text);
|
|
207
|
+
const menuPresent = flags.userInvocable !== false && flags.disableModel !== true;
|
|
208
|
+
const re = referenceRe(name);
|
|
209
|
+
const cmdHits = commandText.filter(([, t]) => re.test(t)).map(([rel]) => rel);
|
|
210
|
+
const flowHits = flowDecl.filter(([, names]) => names.has(name)).map(([rel]) => rel);
|
|
211
|
+
rows.push({
|
|
212
|
+
name,
|
|
213
|
+
commandRefs: cmdHits.length,
|
|
214
|
+
flowRefs: flowHits.length,
|
|
215
|
+
menuPresent,
|
|
216
|
+
hasTriggerCorpus: fs.existsSync(path.join(skillsDir, name, 'evals', 'triggers.json')),
|
|
217
|
+
entryPath: classify(cmdHits.length, flowHits.length, menuPresent),
|
|
218
|
+
firstRef: cmdHits[0] ?? flowHits[0] ?? '',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return rows;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function tally(rows: readonly SkillRow[]): Record<EntryPath, number> {
|
|
225
|
+
const t: Record<EntryPath, number> = {
|
|
226
|
+
both: 0,
|
|
227
|
+
'command-only': 0,
|
|
228
|
+
'flow-only': 0,
|
|
229
|
+
'model-routed': 0,
|
|
230
|
+
orphan: 0,
|
|
231
|
+
};
|
|
232
|
+
for (const r of rows) t[r.entryPath] += 1;
|
|
233
|
+
return t;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function renderArtifact(rows: readonly SkillRow[], pin: string): string {
|
|
237
|
+
const t = tally(rows);
|
|
238
|
+
const candidates = t['command-only'] + t['flow-only'];
|
|
239
|
+
const L: string[] = [];
|
|
240
|
+
L.push('<!-- evidence-type: analysis -->');
|
|
241
|
+
L.push('# Skill menu census — every skill, and what can reach it');
|
|
242
|
+
L.push('');
|
|
243
|
+
L.push(`Pinned to commit \`${pin}\`. Generated by \`./scripts-run src/scripts/report_skill_menu_census --emit --pin ${pin}\`;`);
|
|
244
|
+
L.push('re-running it at the same pin over the same tree is byte-identical, which is what');
|
|
245
|
+
L.push('`road-to-skill-menu-economy` step 1.1 asks to be checkable.');
|
|
246
|
+
L.push('');
|
|
247
|
+
L.push('## The rule this census applied');
|
|
248
|
+
L.push('');
|
|
249
|
+
L.push('**Commands.** A `src/domains/**/command.md` file counts as reaching a skill when it');
|
|
250
|
+
L.push('references it in one of three disambiguating shapes — `skill:<name>`, a `skills/<name>`');
|
|
251
|
+
L.push('path, or the bare name in backticks. A plain word-boundary match is NOT used: names');
|
|
252
|
+
L.push('like `security`, `database` and `docker` are ordinary English and would hit on every');
|
|
253
|
+
L.push('prose mention. The `skill:<name>` shape is closed with a negative lookahead rather');
|
|
254
|
+
L.push('than a word boundary: `\\b` sits between `w` and `-`, so `skill:code-review-lens` was a');
|
|
255
|
+
L.push('hit for `code-review` and a skill would have inherited a longer sibling\'s references.');
|
|
256
|
+
L.push('Tightening it moved no figure in this corpus, which is recorded rather than assumed.');
|
|
257
|
+
L.push('');
|
|
258
|
+
L.push('**Flows.** A `src/flows/*.yaml` file counts when its own `skills:` list names the skill.');
|
|
259
|
+
L.push('Flows carry YAML name lists rather than the shapes above, so applying the command regex');
|
|
260
|
+
L.push('to them returned `flow_refs = 0` for all 299 skills on the first pass. That was a false');
|
|
261
|
+
L.push('zero, corrected before publication; a bare name is safe here precisely because the');
|
|
262
|
+
L.push('`skills:` key bounds where it may be read.');
|
|
263
|
+
L.push('');
|
|
264
|
+
L.push('**Menu.** `menu_present` is true unless the frontmatter carries `user-invocable: false`');
|
|
265
|
+
L.push('or `disable-model-invocation: true`.');
|
|
266
|
+
L.push('');
|
|
267
|
+
L.push('| Class | Meaning |');
|
|
268
|
+
L.push('|---|---|');
|
|
269
|
+
L.push('| `both` | reachable from a command AND from a flow |');
|
|
270
|
+
L.push('| `command-only` | a command references it; no flow does |');
|
|
271
|
+
L.push('| `flow-only` | a flow references it; no command does |');
|
|
272
|
+
L.push('| `model-routed` | neither, and it is on the menu — the menu is the only entry path this tree can point at |');
|
|
273
|
+
L.push('| `orphan` | neither, and it is off the menu: nothing here reaches it |');
|
|
274
|
+
L.push('');
|
|
275
|
+
L.push('## What this census does NOT establish');
|
|
276
|
+
L.push('');
|
|
277
|
+
L.push('It does not decide **sole entry path**. A command reference proves a command CAN');
|
|
278
|
+
L.push('reach the skill; nothing here proves the menu never does. `command-only` and');
|
|
279
|
+
L.push('`flow-only` are therefore CANDIDATES for `user-invocable: false` and never the');
|
|
280
|
+
L.push('marking decision — the roadmap\'s Risk 1 is exactly this misclassification, and its');
|
|
281
|
+
L.push('stated mitigation is a successful command invocation per marked skill, which is a');
|
|
282
|
+
L.push('step 1.2 obligation and not something a static scan can discharge.');
|
|
283
|
+
L.push('');
|
|
284
|
+
L.push('An `orphan` row is a question, not a deletion list (K3).');
|
|
285
|
+
L.push('');
|
|
286
|
+
L.push('## Totals');
|
|
287
|
+
L.push('');
|
|
288
|
+
L.push('| Class | Count |');
|
|
289
|
+
L.push('|---|---:|');
|
|
290
|
+
for (const k of ['both', 'command-only', 'flow-only', 'model-routed', 'orphan'] as EntryPath[]) {
|
|
291
|
+
L.push(`| \`${k}\` | ${String(t[k])} |`);
|
|
292
|
+
}
|
|
293
|
+
L.push(`| **total** | **${String(rows.length)}** |`);
|
|
294
|
+
L.push('');
|
|
295
|
+
L.push(`Menu-removal candidates (\`command-only\` + \`flow-only\`): **${String(candidates)}**.`);
|
|
296
|
+
L.push('');
|
|
297
|
+
L.push('## Rows');
|
|
298
|
+
L.push('');
|
|
299
|
+
L.push('| Skill | Entry path | cmd refs | flow refs | on menu | triggers.json | first reference |');
|
|
300
|
+
L.push('|---|---|---:|---:|:-:|:-:|---|');
|
|
301
|
+
for (const r of rows) {
|
|
302
|
+
L.push(
|
|
303
|
+
`| \`${r.name}\` | \`${r.entryPath}\` | ${String(r.commandRefs)} | ${String(r.flowRefs)} | ` +
|
|
304
|
+
`${r.menuPresent ? 'yes' : 'no'} | ${r.hasTriggerCorpus ? 'yes' : 'no'} | ` +
|
|
305
|
+
`${r.firstRef === '' ? '—' : `\`${r.firstRef}\``} |`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
L.push('');
|
|
309
|
+
return L.join('\n');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function headSha(root: string): string {
|
|
313
|
+
return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function main(argv: readonly string[]): number {
|
|
317
|
+
const root = REPO_ROOT;
|
|
318
|
+
const emit = argv.includes('--emit');
|
|
319
|
+
const pinIdx = argv.indexOf('--pin');
|
|
320
|
+
const pin = pinIdx !== -1 && pinIdx + 1 < argv.length ? (argv[pinIdx + 1] as string) : headSha(root);
|
|
321
|
+
const rows = census(root);
|
|
322
|
+
const t = tally(rows);
|
|
323
|
+
if (emit) {
|
|
324
|
+
const out = path.join(root, ARTIFACT_REL);
|
|
325
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
326
|
+
fs.writeFileSync(out, renderArtifact(rows, pin), 'utf-8');
|
|
327
|
+
process.stdout.write(`wrote ${ARTIFACT_REL} · ${String(rows.length)} row(s) · pin ${pin}\n`);
|
|
328
|
+
}
|
|
329
|
+
process.stdout.write(`scanned: ${String(rows.length)} skill(s)\n`);
|
|
330
|
+
for (const k of ['both', 'command-only', 'flow-only', 'model-routed', 'orphan'] as EntryPath[]) {
|
|
331
|
+
process.stdout.write(` ${k.padEnd(14)} ${String(t[k]).padStart(4)}\n`);
|
|
332
|
+
}
|
|
333
|
+
process.stdout.write(
|
|
334
|
+
` menu-removal candidates (command-only + flow-only): ${String(t['command-only'] + t['flow-only'])}\n`,
|
|
335
|
+
);
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
340
|
+
process.exit(main(process.argv.slice(2)));
|
|
341
|
+
}
|