@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
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
26
27
|
import * as fs from 'node:fs';
|
|
27
28
|
import * as path from 'node:path';
|
|
28
29
|
import { fileURLToPath } from 'node:url';
|
|
@@ -206,6 +207,220 @@ export function classifyPayload(files: readonly PackFile[]): string[] {
|
|
|
206
207
|
return out;
|
|
207
208
|
}
|
|
208
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Payload TYPE classification — what an entry IS, not where it sits.
|
|
212
|
+
*
|
|
213
|
+
* `CONTENT_CLASSES` above matches PATHS. That catches the shapes with a naming
|
|
214
|
+
* convention (a `.pem`, a `.test.js`, a `.vscode/` directory) and is blind to
|
|
215
|
+
* everything else: a compiled binary named `helper`, an archive named
|
|
216
|
+
* `data.md`, an extensionless script nobody looked at. Extension is the
|
|
217
|
+
* attacker's choice; the first bytes are not.
|
|
218
|
+
*
|
|
219
|
+
* Priority is archive > binary > dotfile > no-extension > text, so each entry
|
|
220
|
+
* lands in exactly one class and a count is a partition of the payload.
|
|
221
|
+
*/
|
|
222
|
+
export type PayloadType = 'text' | 'dotfile' | 'no-extension' | 'binary' | 'archive';
|
|
223
|
+
|
|
224
|
+
export const PAYLOAD_TYPES: readonly PayloadType[] = [
|
|
225
|
+
'archive',
|
|
226
|
+
'binary',
|
|
227
|
+
'dotfile',
|
|
228
|
+
'no-extension',
|
|
229
|
+
'text',
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
/** Leading magic bytes for the archive container formats worth naming. */
|
|
233
|
+
const ARCHIVE_MAGIC: ReadonlyArray<readonly number[]> = [
|
|
234
|
+
[0x50, 0x4b, 0x03, 0x04], // zip / jar / docx / xlsx
|
|
235
|
+
[0x1f, 0x8b], // gzip
|
|
236
|
+
[0x42, 0x5a, 0x68], // bzip2
|
|
237
|
+
[0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00], // xz
|
|
238
|
+
[0x04, 0x22, 0x4d, 0x18], // lz4
|
|
239
|
+
[0x28, 0xb5, 0x2f, 0xfd], // zstd
|
|
240
|
+
[0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c], // 7z
|
|
241
|
+
[0x52, 0x61, 0x72, 0x21], // rar
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/** How many leading bytes are read to decide. */
|
|
245
|
+
export const MAGIC_BYTES_READ = 512;
|
|
246
|
+
|
|
247
|
+
function startsWith(buf: Uint8Array, sig: readonly number[]): boolean {
|
|
248
|
+
if (buf.length < sig.length) return false;
|
|
249
|
+
return sig.every((b, i) => buf[i] === b);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Classify one entry from its name and its first bytes.
|
|
254
|
+
*
|
|
255
|
+
* `head` is null when the file could not be read. That is NOT text: an entry
|
|
256
|
+
* the check could not open is an entry it did not classify, and the caller
|
|
257
|
+
* treats it as a failure rather than letting it fall through to the class that
|
|
258
|
+
* needs no exception.
|
|
259
|
+
*/
|
|
260
|
+
export function classifyEntry(entryPath: string, head: Uint8Array | null): PayloadType | 'unreadable' {
|
|
261
|
+
if (head === null) return 'unreadable';
|
|
262
|
+
if (ARCHIVE_MAGIC.some((sig) => startsWith(head, sig))) return 'archive';
|
|
263
|
+
if (head.length > 0) {
|
|
264
|
+
// A NUL byte is decisive. Short of that, a high proportion of C0
|
|
265
|
+
// control bytes outside tab/newline/CR is what separates a compiled
|
|
266
|
+
// object from prose that happens to contain an unusual character.
|
|
267
|
+
if (head.includes(0)) return 'binary';
|
|
268
|
+
let control = 0;
|
|
269
|
+
for (const b of head) {
|
|
270
|
+
if (b < 0x09 || (b > 0x0d && b < 0x20)) control += 1;
|
|
271
|
+
}
|
|
272
|
+
if (control / head.length > 0.1) return 'binary';
|
|
273
|
+
}
|
|
274
|
+
const base = entryPath.split('/').pop() ?? entryPath;
|
|
275
|
+
if (base.startsWith('.')) return 'dotfile';
|
|
276
|
+
if (!base.includes('.')) return 'no-extension';
|
|
277
|
+
return 'text';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* An entry admitted into a class that would otherwise be empty.
|
|
282
|
+
*
|
|
283
|
+
* BOUND, not allow-listed, and the distinction is the whole point. A count
|
|
284
|
+
* ratchet ("8 extensionless entries are fine") is satisfied by ANY eight
|
|
285
|
+
* extensionless entries, so swapping one for something else passes silently —
|
|
286
|
+
* the same key-lookup weakness the Phase 5 pragma work removed one layer up.
|
|
287
|
+
* Here the binding is `sha256(path + "\n" + size)`: change the file and the
|
|
288
|
+
* exception stops applying, which re-fires the check on the entry that changed.
|
|
289
|
+
*
|
|
290
|
+
* NOT a `*_allowlist.json`. There is no file to grow, each entry states its own
|
|
291
|
+
* reason next to the class it excepts, and adding one is a reviewed code change
|
|
292
|
+
* in the gate — the same shape `CONTENT_CLASSES` already uses for its ratchets.
|
|
293
|
+
*
|
|
294
|
+
* Recompute a fingerprint with:
|
|
295
|
+
* ./scripts-run src/scripts/check_pack_size (the failure names path and size)
|
|
296
|
+
*/
|
|
297
|
+
export interface BoundPayloadException {
|
|
298
|
+
readonly fingerprint: string;
|
|
299
|
+
readonly path: string;
|
|
300
|
+
readonly size: number;
|
|
301
|
+
readonly why: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function payloadFingerprint(entryPath: string, size: number): string {
|
|
305
|
+
return createHash('sha256').update(`${entryPath}\n${String(size)}`).digest('hex');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Measured on the BUILT payload, 2026-09-07: 3041 entries, of which 3026 are
|
|
310
|
+
* text and these 15 are not. `archive` is empty and stays a hard zero.
|
|
311
|
+
*
|
|
312
|
+
* The roadmap step that added this expected `binary` to be zero too. It is not:
|
|
313
|
+
* three 69-byte PNG placeholders ship as media-adapter fixtures. Recorded as a
|
|
314
|
+
* correction rather than resolved by redefining the class — they are genuinely
|
|
315
|
+
* binary, they genuinely ship, and dropping them from the published surface is
|
|
316
|
+
* a consumer-visible change that belongs to whoever owns those adapters.
|
|
317
|
+
*/
|
|
318
|
+
const BOUND_PAYLOAD_EXCEPTIONS: readonly BoundPayloadException[] = [
|
|
319
|
+
{ fingerprint: '955b5da399e14fb39cfe1f3279044061043e64e2bd1576871514db927e56d701', path: 'src/scripts/media/lib/fixtures/flux/asset-0001.png', size: 69, why: 'media-adapter response fixture — a 69-byte placeholder PNG the flux adapter test decodes; binary by construction' },
|
|
320
|
+
{ fingerprint: 'c2dd9743367ece8ffe0deef5e798d39967c22245749fd7f798edd8aa78b70f59', path: 'src/scripts/media/lib/fixtures/gemini-image/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
|
|
321
|
+
{ fingerprint: 'cbac44d021a1dc41b938e798eb9be4147fc9d35194c348dfe9acaa0596bc83a5', path: 'src/scripts/media/lib/fixtures/ideogram/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
|
|
322
|
+
{ fingerprint: 'fdf6334f2342a48faaea708fd43853066f7057e528dfd1dd6fb5dcf8c0f67845', path: 'agents/templates/.ai-council.yml.example', size: 29872, why: 'shipped council config TEMPLATE — a dotfile because the file it is copied to is one' },
|
|
323
|
+
{ fingerprint: 'd36a53951c184db7481139235056e90b53a853714b3a68b31ba1349ecb95ac1d', path: 'agents/templates/.ai-video.xml.example', size: 9209, why: 'shipped video-provider config template — same reason as the council template' },
|
|
324
|
+
{ fingerprint: 'f83229412cf8cceaadd0cb1934140a0c7c8277df43edc590f370572f6df6da98', path: 'dist/agent-src/templates/agents/.gitattributes.fragment', size: 2140, why: 'gitattributes fragment the installer appends to a consumer repo' },
|
|
325
|
+
{ fingerprint: 'a4f51dba3a66c0f2c3548e77961271ed4d7b5ec8a6661ea359ff044758f0d921', path: 'src/templates/minimal/.agent-settings.yml', size: 1042, why: 'the minimal-install settings template; the dot is the consumer-side filename' },
|
|
326
|
+
{ fingerprint: 'a9cb828c7f3e6ac62d3c55c2fd215779307bf695a065498b1d1e1188210ebea8', path: 'LICENSE', size: 1064, why: 'the licence npm requires in the tarball; extensionless by convention' },
|
|
327
|
+
{ fingerprint: 'e89a3d025a838e295838db68d0d832c701eeca01bb7e6c3983d3a48950f80017', path: 'NOTICE', size: 472, why: 'attribution notice shipped with the licence; extensionless by convention' },
|
|
328
|
+
{ fingerprint: 'c9da3c6e2476a6d2b9610a1fbf638ba9bb78ddd76fc0f799631d601774c32cab', path: 'dist/agent-src/templates/hooks/pre-commit-frontmatter', size: 2537, why: 'git hook template — git requires a hook file to carry no extension' },
|
|
329
|
+
{ fingerprint: 'aeffa474df3f805de07cdc9c6960c4918aa8e51e317fa1c30906d2f0267b7a16', path: 'dist/agent-src/templates/hooks/pre-commit-roadmap-progress', size: 3988, why: 'git hook template — same reason' },
|
|
330
|
+
{ fingerprint: '914f9c34551c5f4c42a70cd8bb0cd7b73078b0c4e7815b551833a3c64c8476d3', path: 'src/scripts/agent-config', size: 2391, why: 'the CLI entry script; an executable entry point carries no extension by convention' },
|
|
331
|
+
{ fingerprint: '0bb1cdd48ac2e9b6a2a56cde25de9fc221d75d42683680284c43118cff796e92', path: 'src/scripts/hooks/shims/php', size: 4439, why: 'shim invoked as the command name `php`; an extension would break the lookup' },
|
|
332
|
+
{ fingerprint: 'b14bdd3cd589848ad8e6d7e5f4ee633dc9005a2a97abba327a9426ce556714da', path: 'src/scripts/install', size: 22049, why: 'the installer entry script; same reason as the CLI entry' },
|
|
333
|
+
{ fingerprint: '1fb564019de468368e34941dffd5eb8bc994364313e02f476ff354ac0da464d7', path: 'src/templates/minimal/overrides-gitkeep', size: 164, why: 'placeholder that becomes .gitkeep on install; extensionless on purpose' },
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
/** Read the first bytes of a payload entry from the tree it was packed from. */
|
|
337
|
+
function readHead(entryPath: string, root: string): Uint8Array | null {
|
|
338
|
+
try {
|
|
339
|
+
const fd = fs.openSync(path.join(root, entryPath), 'r');
|
|
340
|
+
try {
|
|
341
|
+
const buf = Buffer.alloc(MAGIC_BYTES_READ);
|
|
342
|
+
const read = fs.readSync(fd, buf, 0, MAGIC_BYTES_READ, 0);
|
|
343
|
+
return buf.subarray(0, read);
|
|
344
|
+
} finally {
|
|
345
|
+
fs.closeSync(fd);
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface TypeClassification {
|
|
353
|
+
readonly counts: Record<PayloadType | 'unreadable', number>;
|
|
354
|
+
/** Entries in a non-`text` class with no bound exception. */
|
|
355
|
+
readonly unbound: Array<{ path: string; size: number; type: PayloadType | 'unreadable'; fingerprint: string }>;
|
|
356
|
+
/** Bound exceptions matching no entry in this payload. */
|
|
357
|
+
readonly unusedExceptions: string[];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Classify the whole payload by type and report what is not accounted for.
|
|
362
|
+
*
|
|
363
|
+
* Every entry outside `text` needs a bound exception. `archive` has none and is
|
|
364
|
+
* meant to stay that way; `binary` has three, named; the dotfile and
|
|
365
|
+
* extensionless entries are each bound to a path AND a size, so replacing one
|
|
366
|
+
* with different content re-fires the check on that entry rather than passing
|
|
367
|
+
* inside a count.
|
|
368
|
+
*/
|
|
369
|
+
export function classifyPayloadTypes(
|
|
370
|
+
files: readonly PackFile[],
|
|
371
|
+
root = REPO_ROOT,
|
|
372
|
+
head: (p: string) => Uint8Array | null = (p) => readHead(p, root),
|
|
373
|
+
): TypeClassification {
|
|
374
|
+
const counts: Record<PayloadType | 'unreadable', number> = {
|
|
375
|
+
archive: 0,
|
|
376
|
+
binary: 0,
|
|
377
|
+
dotfile: 0,
|
|
378
|
+
'no-extension': 0,
|
|
379
|
+
text: 0,
|
|
380
|
+
unreadable: 0,
|
|
381
|
+
};
|
|
382
|
+
const byFingerprint = new Map(BOUND_PAYLOAD_EXCEPTIONS.map((e) => [e.fingerprint, e]));
|
|
383
|
+
const used = new Set<string>();
|
|
384
|
+
const unbound: TypeClassification['unbound'] = [];
|
|
385
|
+
for (const f of files) {
|
|
386
|
+
const type = classifyEntry(f.path, head(f.path));
|
|
387
|
+
counts[type] += 1;
|
|
388
|
+
if (type === 'text') continue;
|
|
389
|
+
const fingerprint = payloadFingerprint(f.path, f.size);
|
|
390
|
+
if (byFingerprint.has(fingerprint)) {
|
|
391
|
+
used.add(fingerprint);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
unbound.push({ path: f.path, size: f.size, type, fingerprint });
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
counts,
|
|
398
|
+
unbound,
|
|
399
|
+
unusedExceptions: BOUND_PAYLOAD_EXCEPTIONS.filter((e) => !used.has(e.fingerprint)).map((e) => e.path),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** One line naming every type class and its count. */
|
|
404
|
+
export function payloadTypeLine(c: TypeClassification): string {
|
|
405
|
+
const parts = [...PAYLOAD_TYPES, 'unreadable' as const]
|
|
406
|
+
.filter((t) => t !== 'unreadable' || c.counts.unreadable > 0)
|
|
407
|
+
.map((t) => `${t} ${String(c.counts[t])}`);
|
|
408
|
+
return `${parts.join(' · ')} — ${String(c.unbound.length)} unaccounted`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Violations from the type classification, as human-readable lines. */
|
|
412
|
+
export function typeClassViolations(c: TypeClassification): string[] {
|
|
413
|
+
return c.unbound.map(
|
|
414
|
+
(u) =>
|
|
415
|
+
`payload type \`${u.type}\`: ${u.path} (${String(u.size)} bytes) is not accounted for. ` +
|
|
416
|
+
(u.type === 'archive' || u.type === 'binary'
|
|
417
|
+
? 'This class is meant to be empty. '
|
|
418
|
+
: 'Every dotfile and extensionless entry is carried by a path-and-size-bound exception. ') +
|
|
419
|
+
`If it belongs in the published surface, add a BoundPayloadException with ` +
|
|
420
|
+
`fingerprint sha256:${u.fingerprint} and a reason; otherwise exclude it from package.json files[].`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
209
424
|
/** Per-class counts, for the green-path report. */
|
|
210
425
|
export function payloadClassCounts(
|
|
211
426
|
files: readonly PackFile[],
|
|
@@ -377,8 +592,8 @@ function selfTest(): number {
|
|
|
377
592
|
Array.from({ length: n }, (_, i) => `dist/hooks/chunk-${String(i)}.js.map`);
|
|
378
593
|
return runSelfTest({
|
|
379
594
|
gate: 'check_pack_size',
|
|
380
|
-
minCases:
|
|
381
|
-
minRejectCases:
|
|
595
|
+
minCases: 14,
|
|
596
|
+
minRejectCases: 9,
|
|
382
597
|
cases: [
|
|
383
598
|
{
|
|
384
599
|
name: 'credential-shaped: a .pem in the payload is refused',
|
|
@@ -410,6 +625,95 @@ function selfTest(): number {
|
|
|
410
625
|
expect: 'accept',
|
|
411
626
|
run: () => (classifyPayload(builtPayload(...maps(120))).length > 0 ? 1 : 0),
|
|
412
627
|
},
|
|
628
|
+
{
|
|
629
|
+
// MAGIC BYTES, NOT THE EXTENSION. This is the whole reason the
|
|
630
|
+
// type pass exists beside the path pass above: an archive named
|
|
631
|
+
// `.md` is invisible to every path pattern in CONTENT_CLASSES.
|
|
632
|
+
name: 'type/archive: a zip planted under a .md name is classified archive, not text',
|
|
633
|
+
expect: 'reject',
|
|
634
|
+
run: () =>
|
|
635
|
+
typeClassViolations(
|
|
636
|
+
classifyPayloadTypes(packFiles('src/skills/x/SKILL.md'), REPO_ROOT, () =>
|
|
637
|
+
Uint8Array.from([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00]),
|
|
638
|
+
),
|
|
639
|
+
).length > 0
|
|
640
|
+
? 1
|
|
641
|
+
: 0,
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
name: 'type/archive: the SAME path with ordinary text is classified text and passes',
|
|
645
|
+
expect: 'accept',
|
|
646
|
+
run: () =>
|
|
647
|
+
typeClassViolations(
|
|
648
|
+
classifyPayloadTypes(packFiles('src/skills/x/SKILL.md'), REPO_ROOT, () =>
|
|
649
|
+
new TextEncoder().encode('# An ordinary skill\n'),
|
|
650
|
+
),
|
|
651
|
+
).length > 0
|
|
652
|
+
? 1
|
|
653
|
+
: 0,
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: 'type/binary: a NUL-bearing entry is refused however it is named',
|
|
657
|
+
expect: 'reject',
|
|
658
|
+
run: () =>
|
|
659
|
+
typeClassViolations(
|
|
660
|
+
classifyPayloadTypes(packFiles('src/scripts/helper.ts'), REPO_ROOT, () =>
|
|
661
|
+
Uint8Array.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x00, 0x00]),
|
|
662
|
+
),
|
|
663
|
+
).length > 0
|
|
664
|
+
? 1
|
|
665
|
+
: 0,
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: 'type/extensionless: an unbound extensionless entry is refused',
|
|
669
|
+
expect: 'reject',
|
|
670
|
+
run: () =>
|
|
671
|
+
typeClassViolations(
|
|
672
|
+
classifyPayloadTypes(packFiles('src/scripts/newtool'), REPO_ROOT, () =>
|
|
673
|
+
new TextEncoder().encode('#!/bin/sh\n'),
|
|
674
|
+
),
|
|
675
|
+
).length > 0
|
|
676
|
+
? 1
|
|
677
|
+
: 0,
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
// The property a COUNT ratchet cannot have. LICENSE is bound at
|
|
681
|
+
// its measured size; the same path at a different size is a
|
|
682
|
+
// different artifact and the exception stops applying.
|
|
683
|
+
name: 'type/binding: a bound entry whose SIZE changed is refused again',
|
|
684
|
+
expect: 'reject',
|
|
685
|
+
run: () =>
|
|
686
|
+
typeClassViolations(
|
|
687
|
+
classifyPayloadTypes([{ path: 'LICENSE', size: 999999 }], REPO_ROOT, () =>
|
|
688
|
+
new TextEncoder().encode('MIT\n'),
|
|
689
|
+
),
|
|
690
|
+
).length > 0
|
|
691
|
+
? 1
|
|
692
|
+
: 0,
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: 'type/binding: that same entry at its recorded size passes',
|
|
696
|
+
expect: 'accept',
|
|
697
|
+
run: () =>
|
|
698
|
+
typeClassViolations(
|
|
699
|
+
classifyPayloadTypes([{ path: 'LICENSE', size: 1064 }], REPO_ROOT, () =>
|
|
700
|
+
new TextEncoder().encode('MIT\n'),
|
|
701
|
+
),
|
|
702
|
+
).length > 0
|
|
703
|
+
? 1
|
|
704
|
+
: 0,
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
// An entry the check could not open is one it did not classify.
|
|
708
|
+
name: 'type/unreadable: an entry that cannot be read is refused, never assumed text',
|
|
709
|
+
expect: 'reject',
|
|
710
|
+
run: () =>
|
|
711
|
+
typeClassViolations(
|
|
712
|
+
classifyPayloadTypes(packFiles('src/scripts/gone.ts'), REPO_ROOT, () => null),
|
|
713
|
+
).length > 0
|
|
714
|
+
? 1
|
|
715
|
+
: 0,
|
|
716
|
+
},
|
|
413
717
|
{
|
|
414
718
|
name: 'a plain payload passes — the classes are not firing on everything',
|
|
415
719
|
expect: 'accept',
|
|
@@ -477,7 +781,12 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
477
781
|
// still knows the population it was red over.
|
|
478
782
|
process.stdout.write(`scanned: ${String(pack.files.length)}\n`);
|
|
479
783
|
|
|
480
|
-
const
|
|
784
|
+
const types = classifyPayloadTypes(pack.files);
|
|
785
|
+
const errors = [
|
|
786
|
+
...evaluate(budget, pack),
|
|
787
|
+
...classifyPayload(pack.files),
|
|
788
|
+
...typeClassViolations(types),
|
|
789
|
+
];
|
|
481
790
|
const { perSkill, total } = skillBytes(pack.files);
|
|
482
791
|
if (argv.includes('--json')) {
|
|
483
792
|
process.stdout.write(
|
|
@@ -485,6 +794,15 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
485
794
|
);
|
|
486
795
|
return errors.length > 0 ? 1 : 0;
|
|
487
796
|
}
|
|
797
|
+
// Printed BEFORE the verdict and on EVERY path, red included. The counts are
|
|
798
|
+
// what a reader needs to judge a violation: "one unaccounted binary" reads
|
|
799
|
+
// very differently against a payload of 3 binaries than against one of 0.
|
|
800
|
+
process.stdout.write(` payload types: ${payloadTypeLine(types)}\n`);
|
|
801
|
+
for (const unused of types.unusedExceptions) {
|
|
802
|
+
// Not an error: an unbuilt tree legitimately packs fewer files. Named
|
|
803
|
+
// so a stale exception is visible rather than accumulating unread.
|
|
804
|
+
process.stdout.write(` bound exception matched no entry: ${unused}\n`);
|
|
805
|
+
}
|
|
488
806
|
if (errors.length > 0) {
|
|
489
807
|
for (const e of errors) process.stderr.write(`❌ pack size: ${e}\n`);
|
|
490
808
|
return 1;
|
|
@@ -72,13 +72,17 @@ import {
|
|
|
72
72
|
derive_categories,
|
|
73
73
|
parse_git_log,
|
|
74
74
|
mix_response_blockers,
|
|
75
|
+
previous_promise,
|
|
75
76
|
previous_release_tag,
|
|
77
|
+
promise_readback_blockers,
|
|
76
78
|
stale_draft_labels,
|
|
77
79
|
} from './_lib/release_highlights.js';
|
|
78
80
|
import { loadTaxonomy, measureRange } from './measure_release_mix.js';
|
|
79
81
|
import {
|
|
80
82
|
CURATED_HEAD_INSTRUCTION,
|
|
83
|
+
PROMISE_READBACK_MARKER,
|
|
81
84
|
extract_changelog_section,
|
|
85
|
+
previous_changelog_version,
|
|
82
86
|
} from './_lib/release_material.js';
|
|
83
87
|
import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
84
88
|
|
|
@@ -207,7 +211,8 @@ export function main(argv: readonly string[]): number {
|
|
|
207
211
|
}
|
|
208
212
|
throw err;
|
|
209
213
|
}
|
|
210
|
-
const
|
|
214
|
+
const changelogText = fs.readFileSync(changelogPath, 'utf-8');
|
|
215
|
+
const section = extract_changelog_section(changelogText, version);
|
|
211
216
|
if (!section) {
|
|
212
217
|
process.stderr.write(`CHANGELOG carries no section for ${version}\n`);
|
|
213
218
|
return 2;
|
|
@@ -251,6 +256,11 @@ export function main(argv: readonly string[]): number {
|
|
|
251
256
|
return mix;
|
|
252
257
|
}
|
|
253
258
|
|
|
259
|
+
const readback = check_previous_promise_readback(changelogText, section.body, version);
|
|
260
|
+
if (readback !== 0) {
|
|
261
|
+
return readback;
|
|
262
|
+
}
|
|
263
|
+
|
|
254
264
|
const curated = parse_curated_head(section.body);
|
|
255
265
|
if (!curated) {
|
|
256
266
|
process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
|
|
@@ -417,6 +427,69 @@ export function check_governance_mix_response(
|
|
|
417
427
|
return 1;
|
|
418
428
|
}
|
|
419
429
|
|
|
430
|
+
/**
|
|
431
|
+
* The previous release's promise, read back at the next release.
|
|
432
|
+
*
|
|
433
|
+
* `_lib/release_material.ts` generates `Next cycle ships …` and, until this
|
|
434
|
+
* check existed, nothing ever asked what became of it: 14.18.0 promised the MCP
|
|
435
|
+
* bridge repair, and no gate in the tree would have noticed the same promise
|
|
436
|
+
* being reprinted in 14.19.0 and 14.20.0. A commitment nothing reads back is
|
|
437
|
+
* free to restate, and one that is free to restate is not a commitment.
|
|
438
|
+
*
|
|
439
|
+
* Only the section under release is read, and only the previous section's
|
|
440
|
+
* governance-response block — the same scope every other check here uses, so a
|
|
441
|
+
* historical section cannot red a future release.
|
|
442
|
+
*
|
|
443
|
+
* The check is about ANSWERING, never about having kept the promise. A gate
|
|
444
|
+
* that refused an unkept promise would price honesty above silence and teach
|
|
445
|
+
* the next author to promise nothing.
|
|
446
|
+
*/
|
|
447
|
+
export function check_previous_promise_readback(
|
|
448
|
+
changelogText: string,
|
|
449
|
+
body: string,
|
|
450
|
+
version: string,
|
|
451
|
+
): number {
|
|
452
|
+
const previousVersion = previous_changelog_version(changelogText, version);
|
|
453
|
+
if (previousVersion === null) {
|
|
454
|
+
process.stdout.write(`✅ no previous CHANGELOG section before ${version} — no promise to read back\n`);
|
|
455
|
+
return 0;
|
|
456
|
+
}
|
|
457
|
+
const promise = previous_promise(changelogText, previousVersion);
|
|
458
|
+
if (promise === null) {
|
|
459
|
+
process.stdout.write(
|
|
460
|
+
`✅ the ${previousVersion} section made no next-cycle promise — nothing owed by ${version}\n`,
|
|
461
|
+
);
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
const blockers = promise_readback_blockers(
|
|
465
|
+
body,
|
|
466
|
+
version,
|
|
467
|
+
previousVersion,
|
|
468
|
+
promise,
|
|
469
|
+
`\`release/${version}\``,
|
|
470
|
+
);
|
|
471
|
+
if (blockers.length === 0) {
|
|
472
|
+
process.stdout.write(`✅ ${version} answers the ${previousVersion} next-cycle promise\n`);
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
process.stderr.write(
|
|
476
|
+
`❌ the ${version} section owes an answer to the ${previousVersion} next-cycle promise.\n` +
|
|
477
|
+
blockers.map((b) => ` - ${b}\n`).join('') +
|
|
478
|
+
` The ${previousVersion} promise reads:\n` +
|
|
479
|
+
promise
|
|
480
|
+
.split('\n')
|
|
481
|
+
.map((l) => ` ${l}\n`)
|
|
482
|
+
.join('') +
|
|
483
|
+
` Answering it is one blockquote line under the curated head, e.g.\n` +
|
|
484
|
+
` > ${PROMISE_READBACK_MARKER} the <promise> did not ship; <what happened>.\n` +
|
|
485
|
+
' Shipped, did not ship, or withdrawn with a reason — the check refuses a MISSING\n' +
|
|
486
|
+
' answer, never a particular outcome. An unkept promise answered honestly passes.\n' +
|
|
487
|
+
' Reproduce locally, before any push:\n' +
|
|
488
|
+
` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
|
|
489
|
+
);
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
|
|
420
493
|
const _isMain = (() => {
|
|
421
494
|
const entry = process.argv[1];
|
|
422
495
|
if (!entry) return false;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* The published skill-activation figure agrees with the record it came from.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS A SEPARATE SCRIPT FROM THE CENSUS
|
|
6
|
+
* ---------------------------------------------
|
|
7
|
+
* `report_skill_activation` is a REPORT: it gates on nothing, has no threshold,
|
|
8
|
+
* and its own docstring says it must never acquire one. This is a GATE, and the
|
|
9
|
+
* two must not share an entry point — `src/config/gate-coverage.yml` is a
|
|
10
|
+
* manifest of gates, and `_lib/gate_population.ts` classifies a script by its
|
|
11
|
+
* prefix precisely so that "is this a gate" is answered structurally rather
|
|
12
|
+
* than by whoever last edited a config. Registering a `report_*` id there forks
|
|
13
|
+
* that population, which `tests/scripts/gate_population.test.ts` refuses.
|
|
14
|
+
*
|
|
15
|
+
* So the split is not cosmetic. The report keeps its exit-0 contract; the
|
|
16
|
+
* assertion that CAN fail lives here, where a floor and a self-test belong.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IT CANNOT DO, STATED SO NOBODY READS IT AS COVERED
|
|
19
|
+
* ------------------------------------------------------
|
|
20
|
+
* It compares two COMMITTED files — the census record against the sentence
|
|
21
|
+
* `docs/CLAIMS.md` publishes. It cannot tell whether the record still describes
|
|
22
|
+
* the world: the transcript store it was taken from is one machine's, keyed on
|
|
23
|
+
* the directory the census ran in, and absent from CI. Re-taking the
|
|
24
|
+
* measurement is `report_skill_activation --emit`, which refuses to write from
|
|
25
|
+
* an empty store. This gate only forbids the published sentence and the record
|
|
26
|
+
* from telling two different stories.
|
|
27
|
+
*
|
|
28
|
+
* Exit: 0 agree · 1 disagree or a file is missing.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as os from 'node:os';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
37
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
38
|
+
import {
|
|
39
|
+
CLAIMS_PATH,
|
|
40
|
+
CLAIM_ID,
|
|
41
|
+
RECORD_PATH,
|
|
42
|
+
claimBlock,
|
|
43
|
+
claimProblems,
|
|
44
|
+
parseClaimFigures,
|
|
45
|
+
readRecord,
|
|
46
|
+
} from './report_skill_activation.js';
|
|
47
|
+
|
|
48
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
49
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
50
|
+
|
|
51
|
+
export function check(root: string): number {
|
|
52
|
+
const rec = readRecord(root);
|
|
53
|
+
if (rec === null) {
|
|
54
|
+
process.stderr.write(`❌ ${RECORD_PATH} is missing — the published figure has no record behind it.\n`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const claimsFile = path.join(root, CLAIMS_PATH);
|
|
58
|
+
if (!fs.existsSync(claimsFile)) {
|
|
59
|
+
process.stderr.write(`❌ ${CLAIMS_PATH} is missing.\n`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const block = claimBlock(fs.readFileSync(claimsFile, 'utf8'), CLAIM_ID);
|
|
63
|
+
// Emitted before the verdict on both paths: a gate that reports what it
|
|
64
|
+
// inspected only when it passes leaves the coverage census blind exactly when
|
|
65
|
+
// it matters.
|
|
66
|
+
reportScanned({
|
|
67
|
+
gate: 'check_skill_activation_claim',
|
|
68
|
+
scanned: block === null ? 0 : 1,
|
|
69
|
+
units: 'published claim block(s)',
|
|
70
|
+
roots: [CLAIMS_PATH],
|
|
71
|
+
});
|
|
72
|
+
if (block === null) {
|
|
73
|
+
process.stderr.write(`❌ claim \`${CLAIM_ID}\` is not in ${CLAIMS_PATH}.\n`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const problems = claimProblems(rec, parseClaimFigures(block));
|
|
77
|
+
if (problems.length > 0) {
|
|
78
|
+
process.stderr.write('❌ the published census claim and its record disagree:\n');
|
|
79
|
+
for (const p of problems) {
|
|
80
|
+
process.stderr.write(` · ${p}\n`);
|
|
81
|
+
}
|
|
82
|
+
process.stderr.write(
|
|
83
|
+
`\nRe-take the measurement (report_skill_activation --emit) and update the claim from ${RECORD_PATH}.\n`,
|
|
84
|
+
);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(`✅ census claim matches ${RECORD_PATH} (measured ${rec.measured_at}).\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The gate proving it still discriminates.
|
|
93
|
+
*
|
|
94
|
+
* The third case is the one a value comparison alone would miss: a claim that
|
|
95
|
+
* silently stops stating a figure would otherwise pass every equality test it
|
|
96
|
+
* no longer participates in.
|
|
97
|
+
*/
|
|
98
|
+
function selfTest(): number {
|
|
99
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'activation-claim-selftest-'));
|
|
100
|
+
const record = {
|
|
101
|
+
schema_version: 1,
|
|
102
|
+
measured_at: '2026-01-02',
|
|
103
|
+
skills_shipped: 300,
|
|
104
|
+
with_trigger_key: 10,
|
|
105
|
+
with_trigger_corpus: 100,
|
|
106
|
+
trigger_key_and_corpus: 5,
|
|
107
|
+
human_named_only: 195,
|
|
108
|
+
sessions: 7,
|
|
109
|
+
assistant_turns: 1234,
|
|
110
|
+
invocations: 0,
|
|
111
|
+
distinct_skills_invoked: 0,
|
|
112
|
+
stores: ['fixture'],
|
|
113
|
+
};
|
|
114
|
+
const agreeing =
|
|
115
|
+
'`report_skill_activation` over 7 sessions and 1,234 assistant turns records 0 Skill ' +
|
|
116
|
+
'invocations and 0 of 300 distinct skills (measured 2026-01-02). 10 declare a ' +
|
|
117
|
+
'machine-matchable trigger key in frontmatter, 100 carry an `evals/triggers.json` corpus, ' +
|
|
118
|
+
'5 do both, and the remaining 195 are reachable only by a human naming them.';
|
|
119
|
+
const write = (name: string, body: string): string => {
|
|
120
|
+
const root = path.join(tmp, name);
|
|
121
|
+
fs.mkdirSync(path.join(root, path.dirname(RECORD_PATH)), { recursive: true });
|
|
122
|
+
fs.mkdirSync(path.join(root, path.dirname(CLAIMS_PATH)), { recursive: true });
|
|
123
|
+
fs.writeFileSync(path.join(root, RECORD_PATH), JSON.stringify(record));
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
path.join(root, CLAIMS_PATH),
|
|
126
|
+
`# Claims\n\n### claim: ${CLAIM_ID}\n- claim: ${body}\n- kind: quant\n- status: backed\n- last_verified: 2026-01-02\n\n### claim: unrelated\n- claim: x\n`,
|
|
127
|
+
);
|
|
128
|
+
return root;
|
|
129
|
+
};
|
|
130
|
+
const run = (root: string): number =>
|
|
131
|
+
runGateCli(REPO_ROOT, 'src/scripts/check_skill_activation_claim.ts', ['--root', root], root);
|
|
132
|
+
try {
|
|
133
|
+
return runSelfTest({
|
|
134
|
+
gate: 'check_skill_activation_claim',
|
|
135
|
+
minCases: 4,
|
|
136
|
+
minRejectCases: 3,
|
|
137
|
+
cases: [
|
|
138
|
+
{
|
|
139
|
+
name: 'a turn count that moved away from the record is rejected',
|
|
140
|
+
expect: 'reject',
|
|
141
|
+
run: () => run(write('turns', agreeing.replace('1,234 assistant', '9,999 assistant'))),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'a population redrawn to enlarge the human-named remainder is rejected',
|
|
145
|
+
expect: 'reject',
|
|
146
|
+
run: () => run(write('population', agreeing.replace('remaining 195 are', 'remaining 290 are'))),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'a claim that stops stating the population split is rejected',
|
|
150
|
+
expect: 'reject',
|
|
151
|
+
run: () => run(write('dropped', agreeing.slice(0, agreeing.indexOf('. 10 declare') + 1))),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'a claim reproducing every recorded figure passes',
|
|
155
|
+
expect: 'accept',
|
|
156
|
+
run: () => run(write('agreeing', agreeing)),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
} finally {
|
|
161
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function main(argv: readonly string[]): number {
|
|
166
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
167
|
+
process.stdout.write('usage: check_skill_activation_claim [--root DIR] [--self-test]\n');
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
if (argv.includes('--self-test')) {
|
|
171
|
+
return selfTest();
|
|
172
|
+
}
|
|
173
|
+
const i = argv.indexOf('--root');
|
|
174
|
+
return check(i === -1 ? REPO_ROOT : (argv[i + 1] ?? REPO_ROOT));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
178
|
+
process.exit(main(process.argv.slice(2)));
|
|
179
|
+
}
|
|
@@ -150,7 +150,7 @@ function _check_one(p: string, today: PyDate): string[] {
|
|
|
150
150
|
if (age > MAX_AGE_DAYS) {
|
|
151
151
|
errors.push(
|
|
152
152
|
`${rel}: \`last_eval\` ${raw as string} is ${age}d old (> ${MAX_AGE_DAYS}d) ` +
|
|
153
|
-
'— re-run skill_trigger_eval
|
|
153
|
+
'— re-run skill_trigger_eval and bump it',
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
156
|
}
|