@event4u/agent-config 14.18.0 → 14.20.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 +120 -129
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +44 -43
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +14 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +489 -121
- 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 +42 -12
- 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 +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- 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 +34 -33
- 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 +4455 -3025
- package/dist/install/install.mjs +975 -689
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2087 -1134
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +225 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_capability.ts +38 -0
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +57 -86
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +24 -24
- package/src/scripts/release_publication.ts +6 -2
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/skill_scout.ts +69 -0
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -44,7 +44,19 @@
|
|
|
44
44
|
* count descriptions against bare names. Until that runs, "most skills reach the
|
|
45
45
|
* model without a description" is a suspicion.
|
|
46
46
|
*
|
|
47
|
-
* Exit: 0 always, except a usage/IO error (1). Deliberate
|
|
47
|
+
* Exit: the REPORT is 0 always, except a usage/IO error (1). Deliberate — it
|
|
48
|
+
* gates on nothing and must not acquire a threshold.
|
|
49
|
+
*
|
|
50
|
+
* One later mode DOES carry a verdict, and it is not a threshold on the census:
|
|
51
|
+
* `--emit` exits 1 rather than writing a record from an empty store, because a
|
|
52
|
+
* zero-session reading is "no transcripts at this path" and publishing it as an
|
|
53
|
+
* activation figure is the defect this file exists to end.
|
|
54
|
+
*
|
|
55
|
+
* The assertion that the PUBLISHED figure still matches that record is a gate
|
|
56
|
+
* and lives in `check_skill_activation_claim.ts`. It is a separate script on
|
|
57
|
+
* purpose: `src/config/gate-coverage.yml` is a manifest of gates and
|
|
58
|
+
* `_lib/gate_population.ts` classifies one by prefix, so a `report_*` id
|
|
59
|
+
* registered there forks that population.
|
|
48
60
|
*/
|
|
49
61
|
|
|
50
62
|
import * as fs from 'node:fs';
|
|
@@ -53,6 +65,7 @@ import * as path from 'node:path';
|
|
|
53
65
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
54
66
|
|
|
55
67
|
import { projectStoreSlug } from './_lib/cc_transcript.js';
|
|
68
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
56
69
|
|
|
57
70
|
const _HERE = fileURLToPath(import.meta.url);
|
|
58
71
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -73,11 +86,17 @@ export const DETERMINISTIC_RE = /^\s*(?:[-*]\s*)?(?:\*\*)?(MUST|NEVER|ALWAYS)\b/
|
|
|
73
86
|
export interface SkillCensus {
|
|
74
87
|
total: number;
|
|
75
88
|
withTriggerKey: string[];
|
|
89
|
+
withTriggerCorpus: string[];
|
|
76
90
|
withDeterministicObligation: string[];
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
export function censusSkills(root: string): SkillCensus {
|
|
80
|
-
const out: SkillCensus = {
|
|
94
|
+
const out: SkillCensus = {
|
|
95
|
+
total: 0,
|
|
96
|
+
withTriggerKey: [],
|
|
97
|
+
withTriggerCorpus: [],
|
|
98
|
+
withDeterministicObligation: [],
|
|
99
|
+
};
|
|
81
100
|
if (!fs.existsSync(root)) {
|
|
82
101
|
return out;
|
|
83
102
|
}
|
|
@@ -98,6 +117,12 @@ export function censusSkills(root: string): SkillCensus {
|
|
|
98
117
|
if (TRIGGER_KEYS.some((k) => new RegExp(`^${k}`, 'm').test(fm))) {
|
|
99
118
|
out.withTriggerKey.push(entry.name);
|
|
100
119
|
}
|
|
120
|
+
// A test fixture, not a routing input: no host reads it at selection time.
|
|
121
|
+
// Counted because it is the second of the three populations the surface
|
|
122
|
+
// splits into, and it is the one most often mistaken for the first.
|
|
123
|
+
if (fs.existsSync(path.join(root, entry.name, 'evals', 'triggers.json'))) {
|
|
124
|
+
out.withTriggerCorpus.push(entry.name);
|
|
125
|
+
}
|
|
101
126
|
const body = fmEnd === -1 ? text : text.slice(fmEnd + 4);
|
|
102
127
|
if (DETERMINISTIC_RE.test(body)) {
|
|
103
128
|
out.withDeterministicObligation.push(entry.name);
|
|
@@ -178,7 +203,185 @@ function _pct(n: number, d: number): string {
|
|
|
178
203
|
return d === 0 ? 'n/a' : `${((100 * n) / d).toFixed(1)}%`;
|
|
179
204
|
}
|
|
180
205
|
|
|
181
|
-
|
|
206
|
+
/** Repo-relative path of the record the published claim is derived from. */
|
|
207
|
+
export const RECORD_PATH = 'agents/evidence/metrics/skill-activation-census.json';
|
|
208
|
+
export const CLAIMS_PATH = 'docs/CLAIMS.md';
|
|
209
|
+
/** The one ledger entry this census backs. */
|
|
210
|
+
export const CLAIM_ID = 'skill-activation-census-zero';
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The three populations the shipped surface splits into, plus their overlap.
|
|
214
|
+
*
|
|
215
|
+
* They are computed from one pass rather than three greps because the whole
|
|
216
|
+
* point of 2.1 is that they reconcile: `humanNamedOnly` is a remainder, so it
|
|
217
|
+
* cannot be stated independently and then found to disagree.
|
|
218
|
+
*/
|
|
219
|
+
export interface Populations {
|
|
220
|
+
shipped: number;
|
|
221
|
+
triggerKey: number;
|
|
222
|
+
triggerCorpus: number;
|
|
223
|
+
bothKeyAndCorpus: number;
|
|
224
|
+
humanNamedOnly: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function populations(census: SkillCensus): Populations {
|
|
228
|
+
const corpus = new Set(census.withTriggerCorpus);
|
|
229
|
+
const both = census.withTriggerKey.filter((s) => corpus.has(s));
|
|
230
|
+
const eitherOne = new Set([...census.withTriggerKey, ...census.withTriggerCorpus]);
|
|
231
|
+
return {
|
|
232
|
+
shipped: census.total,
|
|
233
|
+
triggerKey: census.withTriggerKey.length,
|
|
234
|
+
triggerCorpus: census.withTriggerCorpus.length,
|
|
235
|
+
bothKeyAndCorpus: both.length,
|
|
236
|
+
humanNamedOnly: census.total - eitherOne.size,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface CensusRecord {
|
|
241
|
+
schema_version: 1;
|
|
242
|
+
measured_at: string;
|
|
243
|
+
skills_shipped: number;
|
|
244
|
+
with_trigger_key: number;
|
|
245
|
+
with_trigger_corpus: number;
|
|
246
|
+
trigger_key_and_corpus: number;
|
|
247
|
+
human_named_only: number;
|
|
248
|
+
sessions: number;
|
|
249
|
+
assistant_turns: number;
|
|
250
|
+
invocations: number;
|
|
251
|
+
distinct_skills_invoked: number;
|
|
252
|
+
stores: string[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function buildRecord(census: SkillCensus, usage: UsageReport[], today: string): CensusRecord {
|
|
256
|
+
const pop = populations(census);
|
|
257
|
+
const distinct = new Set<string>();
|
|
258
|
+
for (const u of usage) {
|
|
259
|
+
for (const k of Object.keys(u.bySkill)) {
|
|
260
|
+
distinct.add(k);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
schema_version: 1,
|
|
265
|
+
measured_at: today,
|
|
266
|
+
skills_shipped: pop.shipped,
|
|
267
|
+
with_trigger_key: pop.triggerKey,
|
|
268
|
+
with_trigger_corpus: pop.triggerCorpus,
|
|
269
|
+
trigger_key_and_corpus: pop.bothKeyAndCorpus,
|
|
270
|
+
human_named_only: pop.humanNamedOnly,
|
|
271
|
+
sessions: usage.reduce((a, u) => a + u.sessions, 0),
|
|
272
|
+
assistant_turns: usage.reduce((a, u) => a + u.assistantTurns, 0),
|
|
273
|
+
invocations: usage.reduce((a, u) => a + u.invocations, 0),
|
|
274
|
+
distinct_skills_invoked: distinct.size,
|
|
275
|
+
stores: usage.map((u) => path.basename(u.store)),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function readRecord(repoRoot: string): CensusRecord | null {
|
|
280
|
+
const file = path.join(repoRoot, RECORD_PATH);
|
|
281
|
+
if (!fs.existsSync(file)) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')) as CensusRecord;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Numbers the published claim asserts, pulled back out of the claim text.
|
|
289
|
+
*
|
|
290
|
+
* Read from prose on purpose: the claim is the surface a reader believes, so
|
|
291
|
+
* comparing the record against a separate machine field would leave the
|
|
292
|
+
* sentence free to drift from both. `null` for any field the claim no longer
|
|
293
|
+
* states in the expected shape — which `checkClaim` reports as a defect rather
|
|
294
|
+
* than passing over, because a claim that stopped stating its figures is not a
|
|
295
|
+
* claim that agrees with the record.
|
|
296
|
+
*/
|
|
297
|
+
export interface ClaimFigures {
|
|
298
|
+
sessions: number | null;
|
|
299
|
+
assistantTurns: number | null;
|
|
300
|
+
invocations: number | null;
|
|
301
|
+
distinct: number | null;
|
|
302
|
+
shipped: number | null;
|
|
303
|
+
triggerKey: number | null;
|
|
304
|
+
triggerCorpus: number | null;
|
|
305
|
+
bothKeyAndCorpus: number | null;
|
|
306
|
+
humanNamedOnly: number | null;
|
|
307
|
+
measuredAt: string | null;
|
|
308
|
+
lastVerified: string | null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function _num(text: string, re: RegExp): number | null {
|
|
312
|
+
const m = re.exec(text);
|
|
313
|
+
return m?.[1] === undefined ? null : Number.parseInt(m[1].replace(/,/gu, ''), 10);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function claimBlock(claimsText: string, id: string): string | null {
|
|
317
|
+
const start = claimsText.indexOf(`### claim: ${id}\n`);
|
|
318
|
+
if (start === -1) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const rest = claimsText.slice(start + 1);
|
|
322
|
+
const end = rest.indexOf('\n### claim: ');
|
|
323
|
+
return end === -1 ? claimsText.slice(start) : claimsText.slice(start, start + 1 + end);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function parseClaimFigures(block: string): ClaimFigures {
|
|
327
|
+
return {
|
|
328
|
+
sessions: _num(block, /over ([\d,]+) sessions and/u),
|
|
329
|
+
assistantTurns: _num(block, /sessions and ([\d,]+) assistant turns/u),
|
|
330
|
+
invocations: _num(block, /records ([\d,]+) Skill invocations/u),
|
|
331
|
+
distinct: _num(block, /Skill invocations and ([\d,]+) of [\d,]+ distinct skills/u),
|
|
332
|
+
shipped: _num(block, /Skill invocations and [\d,]+ of ([\d,]+) distinct skills/u),
|
|
333
|
+
triggerKey: _num(block, /([\d,]+) declare a machine-matchable trigger key/u),
|
|
334
|
+
triggerCorpus: _num(block, /([\d,]+) carry an `evals\/triggers\.json` corpus/u),
|
|
335
|
+
bothKeyAndCorpus: _num(block, /([\d,]+) do both/u),
|
|
336
|
+
humanNamedOnly: _num(block, /([\d,]+) are reachable only by a human naming them/u),
|
|
337
|
+
measuredAt: /measured (\d{4}-\d{2}-\d{2})/u.exec(block)?.[1] ?? null,
|
|
338
|
+
lastVerified: /^- last_verified: (\d{4}-\d{2}-\d{2})/mu.exec(block)?.[1] ?? null,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Disagreements between the committed record and the published claim.
|
|
344
|
+
*
|
|
345
|
+
* This is the whole of what 1.3 buys, so it is worth being exact about what it
|
|
346
|
+
* does NOT buy: it cannot tell whether the RECORD still describes the world,
|
|
347
|
+
* because the transcript store it was taken from is one machine's and is absent
|
|
348
|
+
* from CI. Re-taking the measurement is `--emit`; this only forbids the
|
|
349
|
+
* published sentence and the record from telling two different stories.
|
|
350
|
+
*/
|
|
351
|
+
export function claimProblems(rec: CensusRecord, fig: ClaimFigures): string[] {
|
|
352
|
+
const out: string[] = [];
|
|
353
|
+
const pairs: Array<[string, number, number | null]> = [
|
|
354
|
+
['sessions', rec.sessions, fig.sessions],
|
|
355
|
+
['assistant turns', rec.assistant_turns, fig.assistantTurns],
|
|
356
|
+
['Skill invocations', rec.invocations, fig.invocations],
|
|
357
|
+
['distinct skills invoked', rec.distinct_skills_invoked, fig.distinct],
|
|
358
|
+
['skills shipped', rec.skills_shipped, fig.shipped],
|
|
359
|
+
['skills with a trigger key', rec.with_trigger_key, fig.triggerKey],
|
|
360
|
+
['skills with a triggers.json corpus', rec.with_trigger_corpus, fig.triggerCorpus],
|
|
361
|
+
['skills in both populations', rec.trigger_key_and_corpus, fig.bothKeyAndCorpus],
|
|
362
|
+
['human-named-only skills', rec.human_named_only, fig.humanNamedOnly],
|
|
363
|
+
];
|
|
364
|
+
for (const [label, recorded, claimed] of pairs) {
|
|
365
|
+
if (claimed === null) {
|
|
366
|
+
out.push(`${label}: the claim no longer states this figure; the record says ${String(recorded)}`);
|
|
367
|
+
} else if (claimed !== recorded) {
|
|
368
|
+
out.push(`${label}: claim says ${String(claimed)}, record says ${String(recorded)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const [label, claimed] of [
|
|
372
|
+
['measurement date', fig.measuredAt],
|
|
373
|
+
['last_verified', fig.lastVerified],
|
|
374
|
+
] as Array<[string, string | null]>) {
|
|
375
|
+
if (claimed === null) {
|
|
376
|
+
out.push(`${label}: the claim no longer states it; the record was measured ${rec.measured_at}`);
|
|
377
|
+
} else if (claimed !== rec.measured_at) {
|
|
378
|
+
out.push(`${label}: claim says ${claimed}, record was measured ${rec.measured_at}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function render(census: SkillCensus, usage: UsageReport[], published: CensusRecord | null = null): string {
|
|
182
385
|
const lines: string[] = [];
|
|
183
386
|
lines.push('skill activation census — advisory, gates on nothing');
|
|
184
387
|
lines.push('');
|
|
@@ -193,6 +396,16 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
|
|
|
193
396
|
lines.push(` ${census.withDeterministicObligation.join(', ')}`);
|
|
194
397
|
}
|
|
195
398
|
lines.push('');
|
|
399
|
+
const pop = populations(census);
|
|
400
|
+
lines.push(' populations — the three ways a skill can be reached, reconciled');
|
|
401
|
+
lines.push(` machine-matchable trigger key ${pop.triggerKey}`);
|
|
402
|
+
lines.push(` evals/triggers.json corpus ${pop.triggerCorpus} (a test fixture; no host reads it at routing time)`);
|
|
403
|
+
lines.push(` both of the above ${pop.bothKeyAndCorpus}`);
|
|
404
|
+
lines.push(` human-named only (remainder) ${pop.humanNamedOnly}`);
|
|
405
|
+
lines.push(
|
|
406
|
+
` reconciles ${pop.triggerKey} + ${pop.triggerCorpus} - ${pop.bothKeyAndCorpus} + ${pop.humanNamedOnly} = ${String(pop.triggerKey + pop.triggerCorpus - pop.bothKeyAndCorpus + pop.humanNamedOnly)} of ${pop.shipped}`,
|
|
407
|
+
);
|
|
408
|
+
lines.push('');
|
|
196
409
|
let totalInv = 0;
|
|
197
410
|
const distinct = new Set<string>();
|
|
198
411
|
for (const u of usage) {
|
|
@@ -213,6 +426,65 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
|
|
|
213
426
|
lines.push(` ${[...distinct].sort().join(', ')}`);
|
|
214
427
|
}
|
|
215
428
|
lines.push('');
|
|
429
|
+
const sessionsRead = usage.reduce((a, u) => a + u.sessions, 0);
|
|
430
|
+
if (sessionsRead === 0) {
|
|
431
|
+
// The store is keyed on the directory the process runs in, so a checkout
|
|
432
|
+
// that never hosted a session — a fresh worktree, a CI runner — yields a
|
|
433
|
+
// zero that means "no transcripts at this path". Publishing that as the
|
|
434
|
+
// activation reading would be the defect this census exists to end, so the
|
|
435
|
+
// two zeros are separated here rather than left to the reader.
|
|
436
|
+
lines.push(' EMPTY STORE: 0 sessions were read, so the zero above is "no transcripts at');
|
|
437
|
+
lines.push(' this path", NOT "no activations". It is not a measurement of the surface and');
|
|
438
|
+
lines.push(' must not be published as one. Run this where the store lives, or pass --store.');
|
|
439
|
+
lines.push('');
|
|
440
|
+
}
|
|
441
|
+
if (published !== null) {
|
|
442
|
+
// Staleness has to be readable from the run itself. Without this block the
|
|
443
|
+
// only way to tell a fresh published figure from a rotted one is to open
|
|
444
|
+
// the git log of `docs/CLAIMS.md`, which is exactly the thing 1.3 forbids.
|
|
445
|
+
const live = buildRecord(census, usage, published.measured_at);
|
|
446
|
+
// The two halves drift for different reasons and a single verdict over both
|
|
447
|
+
// would be worthless: the store is rolling, so its counts move between any
|
|
448
|
+
// two runs — including a run in the session that is adding to it. A reader
|
|
449
|
+
// who is told STALE every time stops reading the line. Only the repository
|
|
450
|
+
// half is a defect when it moves, because the published population split
|
|
451
|
+
// then no longer describes the tree it claims to count.
|
|
452
|
+
const REPO_FIELDS: Array<keyof CensusRecord> = [
|
|
453
|
+
'skills_shipped',
|
|
454
|
+
'with_trigger_key',
|
|
455
|
+
'with_trigger_corpus',
|
|
456
|
+
'trigger_key_and_corpus',
|
|
457
|
+
'human_named_only',
|
|
458
|
+
];
|
|
459
|
+
const STORE_FIELDS: Array<keyof CensusRecord> = [
|
|
460
|
+
'sessions',
|
|
461
|
+
'assistant_turns',
|
|
462
|
+
'invocations',
|
|
463
|
+
'distinct_skills_invoked',
|
|
464
|
+
];
|
|
465
|
+
const repoDrift = REPO_FIELDS.filter((k) => live[k] !== published[k]);
|
|
466
|
+
const storeDrift = STORE_FIELDS.filter((k) => live[k] !== published[k]);
|
|
467
|
+
lines.push(` PUBLISHED RECORD (${RECORD_PATH}), measured ${published.measured_at}:`);
|
|
468
|
+
lines.push(
|
|
469
|
+
` sessions=${String(published.sessions)} asst=${String(published.assistant_turns)} invocations=${String(published.invocations)} distinct=${String(published.distinct_skills_invoked)}`,
|
|
470
|
+
);
|
|
471
|
+
if (repoDrift.length > 0) {
|
|
472
|
+
lines.push(` STALE: the published population split no longer describes this tree —`);
|
|
473
|
+
lines.push(` ${repoDrift.join(', ')} moved. Re-take with --emit.`);
|
|
474
|
+
} else {
|
|
475
|
+
lines.push(' population split still describes this tree.');
|
|
476
|
+
}
|
|
477
|
+
if (sessionsRead === 0) {
|
|
478
|
+
lines.push(' Store reading: UNDECIDED — this run read no store, so it cannot say whether');
|
|
479
|
+
lines.push(' the published figures still hold. Re-run where the store lives.');
|
|
480
|
+
} else if (storeDrift.length === 0) {
|
|
481
|
+
lines.push(' Store reading: reproduces the published figures exactly.');
|
|
482
|
+
} else {
|
|
483
|
+
lines.push(` Store reading: moved since the record (${storeDrift.join(', ')}). Expected —`);
|
|
484
|
+
lines.push(' the store is rolling, not append-only, and a later reading can be lower.');
|
|
485
|
+
}
|
|
486
|
+
lines.push('');
|
|
487
|
+
}
|
|
216
488
|
if (census.withTriggerKey.length === 0) {
|
|
217
489
|
lines.push(' VERDICT: activation is not measurable as a rate. No skill declares a');
|
|
218
490
|
lines.push(' machine-matchable trigger, so a missed-activation detector has nothing to');
|
|
@@ -227,19 +499,32 @@ export function render(census: SkillCensus, usage: UsageReport[]): string {
|
|
|
227
499
|
return lines.join('\n');
|
|
228
500
|
}
|
|
229
501
|
|
|
502
|
+
const USAGE =
|
|
503
|
+
'usage: report_skill_activation [--root DIR] [--limit N] [--store PATH]... [--emit] [--allow-empty-store]\n';
|
|
504
|
+
|
|
230
505
|
function main(argv: string[]): number {
|
|
231
506
|
let limit = 30;
|
|
507
|
+
let emit = false;
|
|
508
|
+
let allowEmpty = false;
|
|
509
|
+
const rootIdx = argv.indexOf('--root');
|
|
510
|
+
const root = rootIdx === -1 ? REPO_ROOT : (argv[rootIdx + 1] ?? REPO_ROOT);
|
|
232
511
|
const stores: string[] = [];
|
|
233
512
|
for (let i = 0; i < argv.length; i += 1) {
|
|
234
513
|
const a = argv[i];
|
|
235
|
-
if (a === '--
|
|
514
|
+
if (a === '--root' && argv[i + 1] !== undefined) {
|
|
515
|
+
i += 1;
|
|
516
|
+
} else if (a === '--limit' && argv[i + 1] !== undefined) {
|
|
236
517
|
limit = Number.parseInt(argv[i + 1] as string, 10);
|
|
237
518
|
i += 1;
|
|
238
519
|
} else if (a === '--store' && argv[i + 1] !== undefined) {
|
|
239
520
|
stores.push(argv[i + 1] as string);
|
|
240
521
|
i += 1;
|
|
522
|
+
} else if (a === '--emit') {
|
|
523
|
+
emit = true;
|
|
524
|
+
} else if (a === '--allow-empty-store') {
|
|
525
|
+
allowEmpty = true;
|
|
241
526
|
} else if (a === '--help' || a === '-h') {
|
|
242
|
-
process.stdout.write(
|
|
527
|
+
process.stdout.write(USAGE);
|
|
243
528
|
return 0;
|
|
244
529
|
}
|
|
245
530
|
}
|
|
@@ -250,9 +535,36 @@ function main(argv: string[]): number {
|
|
|
250
535
|
if (stores.length === 0) {
|
|
251
536
|
stores.push(defaultStore(REPO_ROOT));
|
|
252
537
|
}
|
|
253
|
-
const census = censusSkills(path.join(
|
|
538
|
+
const census = censusSkills(path.join(root, SKILLS_ROOT));
|
|
254
539
|
const usage = stores.map((s) => measureUsage(s, limit));
|
|
255
|
-
process.stdout.write(`${render(census, usage)}\n`);
|
|
540
|
+
process.stdout.write(`${render(census, usage, readRecord(root))}\n`);
|
|
541
|
+
// The skills are the corpus this floor can hold: they are in the repository,
|
|
542
|
+
// so the count is the same everywhere. The transcript half is path-scoped and
|
|
543
|
+
// legitimately zero in CI, which is why it is reported above and not here — a
|
|
544
|
+
// floor over it would either red every CI run or be vacuous.
|
|
545
|
+
reportScanned({
|
|
546
|
+
gate: 'report_skill_activation',
|
|
547
|
+
scanned: census.total,
|
|
548
|
+
units: 'SKILL.md file(s)',
|
|
549
|
+
roots: [SKILLS_ROOT],
|
|
550
|
+
});
|
|
551
|
+
if (!emit) {
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
const sessionsRead = usage.reduce((a, u) => a + u.sessions, 0);
|
|
555
|
+
if (sessionsRead === 0 && !allowEmpty) {
|
|
556
|
+
process.stderr.write(
|
|
557
|
+
'report_skill_activation --emit: refusing to write a record from 0 sessions — that zero\n' +
|
|
558
|
+
'is "no transcripts at this path", not an activation reading. Run where the store lives,\n' +
|
|
559
|
+
'pass --store, or pass --allow-empty-store if an empty store is genuinely the finding.\n',
|
|
560
|
+
);
|
|
561
|
+
return 1;
|
|
562
|
+
}
|
|
563
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
564
|
+
const file = path.join(root, RECORD_PATH);
|
|
565
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
566
|
+
fs.writeFileSync(file, `${JSON.stringify(buildRecord(census, usage, today), null, 2)}\n`);
|
|
567
|
+
process.stdout.write(`\nwrote ${RECORD_PATH} (measured ${today})\n`);
|
|
256
568
|
return 0;
|
|
257
569
|
}
|
|
258
570
|
|
|
@@ -51,6 +51,11 @@
|
|
|
51
51
|
"type": "string"
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
|
+
"no_findings_reason": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1,
|
|
57
|
+
"description": "Why the finding set is empty. REQUIRED whenever a ledger file exists and `findings` is empty — enforced in check_finding_dispositions.empty_ledger_problem rather than here, because the requirement is conditional on the array length and this repo's Draft-07 subset validator resolves no `if`/`then`. The field exists because `findings: []` and a missing file made the same claim to every reader while collapsing two states that differ entirely: a review that ran and reported nothing, and a review that never ran. The reason must pick one and say how a reader can check it."
|
|
58
|
+
},
|
|
54
59
|
"findings": {
|
|
55
60
|
"type": "array",
|
|
56
61
|
"description": "May be empty. An empty array is an honest null, not a missing field — the distinction the Markdown track spells with its own §2.3 line. The item shape is INLINED rather than `$ref`-ed for the same measured reason: the subset validator does not resolve `$ref`, so a referenced definition would have validated nothing.",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://github.com/event4u-app/agent-config/scripts/schemas/skill.schema.json",
|
|
4
|
-
"x-schemaVersion": "1.1.
|
|
4
|
+
"x-schemaVersion": "1.1.1",
|
|
5
5
|
"title": "Skill frontmatter",
|
|
6
6
|
"$comment": "Source: agents/reference/docs/frontmatter-contract.md#skills. Keep in sync with inventory_frontmatter.py and the linter's lint_skill().",
|
|
7
7
|
"type": "object",
|
|
@@ -276,7 +276,7 @@
|
|
|
276
276
|
},
|
|
277
277
|
"requires_confirmation": {
|
|
278
278
|
"type": "boolean",
|
|
279
|
-
"description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: only claude both binds pre_tool_use and honours a deny (docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use — 2 further hosts bind it and ignore the verdict, 3 alias it unbound, 2
|
|
279
|
+
"description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: only claude both binds pre_tool_use and honours a deny (docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use — 2 further hosts bind it and ignore the verdict, 3 alias it unbound, 2 carry no pre-tool alias row), so a guard would degrade to prose on 7 of 8 hosts while the flag reads like a control — that decision is deferred behind blocker confirmation-degraded-host-semantics."
|
|
280
280
|
},
|
|
281
281
|
"command": {
|
|
282
282
|
"type": "array",
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
attachReleaseErrors,
|
|
49
49
|
listRecords,
|
|
50
50
|
markReleased,
|
|
51
|
+
writeTargetIndex,
|
|
51
52
|
openRecords,
|
|
52
53
|
readOverflow,
|
|
53
54
|
readRecord,
|
|
@@ -503,6 +504,30 @@ function releaseCmd(root: string, argv: string[]): number {
|
|
|
503
504
|
return EXIT_OK;
|
|
504
505
|
}
|
|
505
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Rebuild the per-target occurrence index and print it.
|
|
509
|
+
*
|
|
510
|
+
* A DERIVED read: the record files are the source, the index is regenerable
|
|
511
|
+
* and gitignored with the rest of `agents/runtime/`. It exists because
|
|
512
|
+
* `occurrences` counts a fingerprint (class + normalised evidence shape), so a
|
|
513
|
+
* rule's weakness showing up in three different shapes was three records with
|
|
514
|
+
* one occurrence each — and the third-recurrence escalation had nothing that
|
|
515
|
+
* could ever establish its own precondition.
|
|
516
|
+
*/
|
|
517
|
+
function targetsCmd(root: string): number {
|
|
518
|
+
const counts = writeTargetIndex(root);
|
|
519
|
+
if (counts.length === 0) {
|
|
520
|
+
process.stdout.write('self-repair: no record carries a resolved target yet.\n');
|
|
521
|
+
return EXIT_OK;
|
|
522
|
+
}
|
|
523
|
+
for (const c of counts) {
|
|
524
|
+
process.stdout.write(
|
|
525
|
+
` ${c.target} ${c.occurrences} occurrence(s) across ${c.records.length} record(s)\n`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return EXIT_OK;
|
|
529
|
+
}
|
|
530
|
+
|
|
506
531
|
export function main(argv: readonly string[]): number {
|
|
507
532
|
const root = process.cwd();
|
|
508
533
|
const sub = argv[0] ?? 'status';
|
|
@@ -512,7 +537,12 @@ export function main(argv: readonly string[]): number {
|
|
|
512
537
|
if (sub === 'release') {
|
|
513
538
|
return releaseCmd(root, [...argv.slice(1)]);
|
|
514
539
|
}
|
|
515
|
-
|
|
540
|
+
if (sub === 'targets') {
|
|
541
|
+
return targetsCmd(root);
|
|
542
|
+
}
|
|
543
|
+
process.stderr.write(
|
|
544
|
+
`self-repair: unknown subcommand '${sub}' (status | release | targets).\n`,
|
|
545
|
+
);
|
|
516
546
|
return EXIT_FAIL;
|
|
517
547
|
}
|
|
518
548
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* rule alone is exactly the kind of surface that is dropped in fresh
|
|
17
17
|
* conversations (found RED baseline on the reply-close PR-URL contract,
|
|
18
18
|
* 2026-07-27); the session_start injection makes the contract present in every
|
|
19
|
-
* new conversation on hook-capable hosts. Copilot (
|
|
19
|
+
* new conversation on hook-capable hosts. Copilot (nothing bound here) falls back
|
|
20
20
|
* to the rule alone.
|
|
21
21
|
*
|
|
22
22
|
* Gate + name resolution (first non-empty wins — the canary is a PERSONAL,
|
|
@@ -37,9 +37,10 @@
|
|
|
37
37
|
*
|
|
38
38
|
* ## Honest coverage
|
|
39
39
|
*
|
|
40
|
-
* The heartbeat is a per-turn obligation and does not reach every host.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* The heartbeat is a per-turn obligation and does not reach every host. This
|
|
41
|
+
* package binds nothing on `copilot`; `cursor`'s per-turn reachability is not
|
|
42
|
+
* established here, so it is excluded rather than assumed; `cowork`'s lifecycle
|
|
43
|
+
* events are wired but inert. Those hosts
|
|
43
44
|
* are named in `HEARTBEAT_REACHABLE_PLATFORMS` by their absence, and a session
|
|
44
45
|
* there simply stops being visible after its TTL rather than being reported as
|
|
45
46
|
* covered.
|
|
@@ -48,6 +48,12 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
48
48
|
|
|
49
49
|
import { collect, _keyword_vector, _cosine } from './audit_skill_overlap.js';
|
|
50
50
|
import { reportScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
51
|
+
import * as sl from './_lib/security_lint.js';
|
|
52
|
+
import { _scan as _scanHiddenUnicode } from './lint_hidden_unicode.js';
|
|
53
|
+
import { _scan as _scanConfusables } from './lint_confusables.js';
|
|
54
|
+
import { _scan as _scanInstructionSmuggling } from './lint_instruction_smuggling.js';
|
|
55
|
+
import { _scan as _scanMcpConfigSecurity } from './lint_mcp_config_security.js';
|
|
56
|
+
import { _scan as _scanFrontmatterSafety } from './lint_skill_frontmatter_safety.js';
|
|
51
57
|
|
|
52
58
|
const _HERE = fileURLToPath(import.meta.url);
|
|
53
59
|
const _DEFAULT_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -262,6 +268,60 @@ export function resolveCandidateDir(
|
|
|
262
268
|
return { dir, refusal: null };
|
|
263
269
|
}
|
|
264
270
|
|
|
271
|
+
/**
|
|
272
|
+
* The five content linters, by the check id each one reports under.
|
|
273
|
+
*
|
|
274
|
+
* The SAME functions the `lint_agent_security` umbrella spawns as processes.
|
|
275
|
+
* Called in-process here rather than shelled out: five `tsx` cold starts per
|
|
276
|
+
* candidate is a cost with no benefit, and the thing that must be identical is
|
|
277
|
+
* the DETECTION, which is these functions.
|
|
278
|
+
*/
|
|
279
|
+
const CONTENT_LINTERS: ReadonlyArray<readonly [string, (sf: sl.ScannedFile) => sl.Finding[]]> = [
|
|
280
|
+
['hidden-unicode', _scanHiddenUnicode],
|
|
281
|
+
['mixed-script-confusable', _scanConfusables],
|
|
282
|
+
['instruction-smuggling', _scanInstructionSmuggling],
|
|
283
|
+
['mcp-config-security', _scanMcpConfigSecurity],
|
|
284
|
+
['dangerous-frontmatter', _scanFrontmatterSafety],
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Read what a quarantined candidate SAYS, and refuse on a payload.
|
|
289
|
+
*
|
|
290
|
+
* `intake`'s other checks are properties of the file — symlink, extension, exec
|
|
291
|
+
* bit, size. Every one of them can be true of a file whose text tells an agent
|
|
292
|
+
* to hide its output from the user. "Inert" was a claim about the container that
|
|
293
|
+
* the gate read as a claim about the contents.
|
|
294
|
+
*
|
|
295
|
+
* TWO DELIBERATE DIFFERENCES FROM HOW THESE LINTERS RUN OVER THIS PACKAGE:
|
|
296
|
+
*
|
|
297
|
+
* 1. **Any HIGH refuses, whatever its weight.** The 0.25 confidence weighting is
|
|
298
|
+
* a false-positive containment device for THIS repository's own corpus, where
|
|
299
|
+
* a path containing `docs` or `examples` really is documentation somebody
|
|
300
|
+
* here wrote. A quarantined third-party candidate carries no such provenance,
|
|
301
|
+
* and a directory named `examples/` in it is an attacker's word for it. A
|
|
302
|
+
* refusal is also cheaper than a false pass in exactly this direction: the
|
|
303
|
+
* candidate is rejected and a human reads it.
|
|
304
|
+
* 2. **Every text extension is scanned, not just `.md`.** The candidate
|
|
305
|
+
* allow-list already bounds what may be present; a payload in the `.yml` a
|
|
306
|
+
* candidate ships is a payload.
|
|
307
|
+
*
|
|
308
|
+
* The refusal NAMES the linter, because "the content scan refused this" sends a
|
|
309
|
+
* reader to five detectors.
|
|
310
|
+
*/
|
|
311
|
+
export function scanCandidateContent(candidateDir: string): string[] {
|
|
312
|
+
const out: string[] = [];
|
|
313
|
+
for (const sf of sl.iter_corpus(['.'], [...ALLOWED_EXT], candidateDir)) {
|
|
314
|
+
for (const [check, scan] of CONTENT_LINTERS) {
|
|
315
|
+
for (const finding of scan(sf)) {
|
|
316
|
+
if (finding.severity !== 'HIGH') continue;
|
|
317
|
+
const loc = finding.line ? `${sf.rel}:${String(finding.line)}` : sf.rel;
|
|
318
|
+
out.push(`${loc}: flagged by ${check} — ${finding.message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
|
|
265
325
|
/**
|
|
266
326
|
* Refuse a candidate that is not inert.
|
|
267
327
|
*
|
|
@@ -296,6 +356,15 @@ export function intake(candidateDir: string): IntakeResult {
|
|
|
296
356
|
if (files.length === 0) {
|
|
297
357
|
refusals.push('candidate directory is empty');
|
|
298
358
|
}
|
|
359
|
+
// Content, after the file properties. Ordering is not cosmetic: a candidate
|
|
360
|
+
// that fails an inertness check may not be safe to READ (a symlink out of
|
|
361
|
+
// quarantine is the case the read-time re-check exists for), so the bytes
|
|
362
|
+
// are only opened once the container has been judged.
|
|
363
|
+
if (refusals.length === 0) {
|
|
364
|
+
for (const flagged of scanCandidateContent(candidateDir)) {
|
|
365
|
+
refusals.push(flagged);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
299
368
|
return { accepted: refusals.length === 0, refusals, files_seen: files.length };
|
|
300
369
|
}
|
|
301
370
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_note": "Shrink-only grandfather allowlist for check_trigger_eval_presence (road-to-composition-ratchet). Skills listed here predate the trigger-eval presence gate and may lack evals/triggers.json. Entries may ONLY be removed (when the skill gains a triggers.json or is deleted)
|
|
2
|
+
"_note": "Shrink-only grandfather allowlist for check_trigger_eval_presence (road-to-composition-ratchet). Skills listed here predate the trigger-eval presence gate and may lack evals/triggers.json. Entries may ONLY be removed (when the skill gains a triggers.json or is deleted) — never added. Frozen 2026-07-08 at 221 of 264 skills.",
|
|
3
|
+
"_no_obligation_note": "Entries in `_no_obligation` are on the `skills` allowlist for a DIFFERENT reason from the rest. The list above records skills that predate the gate and owe a triggers.json; these owe none, because their description says explicit-request-only and they publish no auto-trigger keywords — there is no trigger to evaluate. Listing them without this distinction reads as a waived obligation when the obligation is absent. Read by humans only: check_trigger_eval_presence parses `skills` and ignores every other key, so this changes no verdict. Removing one from `skills` WOULD change a verdict — the gate would then demand a triggers.json for a skill that has nothing to trigger on.",
|
|
4
|
+
"_no_obligation": [
|
|
5
|
+
"sequential-thinking"
|
|
6
|
+
],
|
|
3
7
|
"skills": [
|
|
4
8
|
"accessibility-auditor",
|
|
5
9
|
"activation-design",
|
|
@@ -42,7 +42,7 @@ const SCHEMA_PATH = path.join(REPO_ROOT, 'src', 'scripts', 'schemas', 'agent-set
|
|
|
42
42
|
const TEMPLATE_PATH = path.join(REPO_ROOT, 'src', 'config', 'agent-settings.template.yml');
|
|
43
43
|
const LOCAL_PATHS = [project_settings_path(REPO_ROOT)];
|
|
44
44
|
|
|
45
|
-
// Installer-default substitutions, mirroring
|
|
45
|
+
// Installer-default substitutions, mirroring the installer so the
|
|
46
46
|
// template validates as it would after a fresh `balanced` install.
|
|
47
47
|
const PLACEHOLDERS: Record<string, string> = {
|
|
48
48
|
__RULE_LOADING_TIER__: 'balanced',
|