@event4u/agent-config 9.20.0 → 9.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +51 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-XGA2NTU4.js → chunk-3EWYC3YK.js} +40 -0
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/hooks/dispatch.js +58 -2
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +47 -29
- package/package.json +1 -1
- package/src/config/gate-coverage.yml +22 -4
- package/src/config/gate-violation-baselines.json +3 -8
- package/src/scripts/_lib/gate_population.ts +70 -0
- package/src/scripts/_lib/roadmap_granularity.ts +157 -0
- package/src/scripts/_lib/scan_scope.ts +44 -0
- package/src/scripts/_lib/stdin.ts +82 -0
- package/src/scripts/audit_adr_coverage.ts +38 -0
- package/src/scripts/audit_auto_rules.ts +17 -3
- package/src/scripts/audit_command_surface.ts +51 -0
- package/src/scripts/audit_initial_context.ts +39 -3
- package/src/scripts/audit_mcp_tools.ts +25 -0
- package/src/scripts/audit_overlap.ts +24 -1
- package/src/scripts/audit_skill_descriptions.ts +19 -0
- package/src/scripts/{verify_before_complete_hook.ts → before_complete_hook.ts} +16 -3
- package/src/scripts/check_adr_frontmatter.ts +23 -3
- package/src/scripts/check_always_budget.ts +24 -0
- package/src/scripts/check_artefact_checksums.ts +24 -1
- package/src/scripts/check_artefact_count_messaging.ts +20 -0
- package/src/scripts/check_augmentignore.ts +22 -15
- package/src/scripts/check_backstop_debt.ts +22 -0
- package/src/scripts/check_beta_review_markers.ts +22 -1
- package/src/scripts/check_bridge_derivation.ts +22 -1
- package/src/scripts/check_bundle_path_leakage.ts +26 -0
- package/src/scripts/check_ci_strict_superset.ts +21 -3
- package/src/scripts/check_claims.ts +22 -0
- package/src/scripts/check_cli_registry_budget_sync.ts +33 -4
- package/src/scripts/check_cluster_patterns.ts +26 -4
- package/src/scripts/check_command_count_messaging.ts +20 -4
- package/src/scripts/check_comparison.ts +18 -0
- package/src/scripts/check_condensed_paths.ts +23 -2
- package/src/scripts/check_consumer_scope_flip.ts +21 -2
- package/src/scripts/check_council_config_location.ts +20 -0
- package/src/scripts/check_council_layout.ts +22 -2
- package/src/scripts/check_council_references.ts +23 -1
- package/src/scripts/check_dependency_floors.ts +19 -3
- package/src/scripts/check_discovery_determinism.ts +26 -0
- package/src/scripts/check_enforcement_coverage.ts +20 -0
- package/src/scripts/check_evaluator_budgets.ts +23 -0
- package/src/scripts/check_finding_dispositions.ts +29 -0
- package/src/scripts/check_gate_coverage.ts +63 -22
- package/src/scripts/check_gate_paths.ts +30 -5
- package/src/scripts/check_generated_artefact_headers.ts +21 -0
- package/src/scripts/check_generator_output_coverage.ts +23 -0
- package/src/scripts/check_gitignore_freshness.ts +23 -0
- package/src/scripts/check_host_loadability.ts +34 -8
- package/src/scripts/check_installer_import_purity.ts +22 -8
- package/src/scripts/check_kernel_prefix_stability.ts +19 -1
- package/src/scripts/check_kernel_rule_bundle.ts +23 -0
- package/src/scripts/check_knowledge_cards.ts +13 -0
- package/src/scripts/check_knowledge_pages.ts +23 -0
- package/src/scripts/check_knowledge_sharing.ts +27 -0
- package/src/scripts/check_md_language.ts +24 -0
- package/src/scripts/check_media_deps.ts +23 -0
- package/src/scripts/check_memory.ts +26 -1
- package/src/scripts/check_memory_contradiction.ts +16 -1
- package/src/scripts/check_memory_proposal.ts +15 -0
- package/src/scripts/check_memory_similarity.ts +16 -0
- package/src/scripts/check_module_management_neutral.ts +19 -11
- package/src/scripts/check_no_conflict_markers.ts +22 -1
- package/src/scripts/check_no_external_sources.ts +26 -1
- package/src/scripts/check_no_local_settings_committed.ts +37 -10
- package/src/scripts/check_no_new_legacy_path.ts +30 -1
- package/src/scripts/check_one_off_location.ts +43 -0
- package/src/scripts/check_originality_freshness.ts +25 -1
- package/src/scripts/check_overlay_cascade_subdirs.ts +21 -0
- package/src/scripts/check_pack_size.ts +22 -0
- package/src/scripts/check_preamble_payload_budget.ts +35 -5
- package/src/scripts/check_proposal.ts +24 -1
- package/src/scripts/check_public_catalog_links.ts +27 -0
- package/src/scripts/check_public_links.ts +19 -0
- package/src/scripts/check_quality_regression.ts +19 -0
- package/src/scripts/check_reach_channels.ts +44 -0
- package/src/scripts/check_reach_staleness.ts +27 -0
- package/src/scripts/check_release_adjacent_health.ts +28 -0
- package/src/scripts/check_release_highlights.ts +34 -0
- package/src/scripts/check_release_includes_discovery.ts +25 -6
- package/src/scripts/check_release_pr_shape.ts +18 -1
- package/src/scripts/check_release_published.ts +21 -0
- package/src/scripts/check_release_surface_equality.ts +22 -0
- package/src/scripts/check_release_trunk_sync.ts +19 -0
- package/src/scripts/check_reply_consistency.ts +22 -0
- package/src/scripts/check_roadmap_trackable.ts +20 -0
- package/src/scripts/check_role_doc_links.ts +23 -0
- package/src/scripts/check_rule_invariants.ts +14 -0
- package/src/scripts/check_secret_leak.ts +47 -0
- package/src/scripts/check_skill_gaps.ts +32 -0
- package/src/scripts/check_skill_requires.ts +19 -0
- package/src/scripts/check_source_pointer_freshness.ts +20 -0
- package/src/scripts/check_structural_breaking.ts +29 -2
- package/src/scripts/check_surface_tiers.ts +34 -0
- package/src/scripts/check_template_pin_drift.ts +23 -1
- package/src/scripts/check_test_coverage_diff.ts +17 -1
- package/src/scripts/check_token_optimizer_freshness.ts +18 -3
- package/src/scripts/check_token_quality_golden.ts +21 -0
- package/src/scripts/check_token_regression.ts +25 -0
- package/src/scripts/check_tracked_but_ignored.ts +26 -0
- package/src/scripts/check_trigger_eval_presence.ts +23 -1
- package/src/scripts/check_trigger_evals.ts +36 -0
- package/src/scripts/check_trunk_drift.ts +23 -0
- package/src/scripts/check_update_banner.ts +24 -0
- package/src/scripts/gate_scope_classify.ts +513 -0
- package/src/scripts/hook_manifest.yaml +1 -1
- package/src/scripts/hooks/block_kernel_rule_writes.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/lint_agent_security.ts +19 -0
- package/src/scripts/lint_agent_skill_names.ts +30 -0
- package/src/scripts/lint_agents_layout.ts +20 -0
- package/src/scripts/lint_agents_md.ts +20 -0
- package/src/scripts/lint_archived_skills.ts +24 -3
- package/src/scripts/lint_behavioural_eval_freshness.ts +29 -3
- package/src/scripts/lint_bench_ab.ts +25 -0
- package/src/scripts/lint_bench_corpus.ts +20 -4
- package/src/scripts/lint_breaking_changes_index.ts +24 -0
- package/src/scripts/lint_budget_ownership.ts +20 -3
- package/src/scripts/lint_changelog_rollback.ts +24 -0
- package/src/scripts/lint_command_flow_coverage.ts +22 -0
- package/src/scripts/lint_command_tiers.ts +29 -5
- package/src/scripts/lint_commit_subjects.ts +15 -0
- package/src/scripts/lint_confusables.ts +24 -0
- package/src/scripts/lint_consumer_internal_refs.ts +24 -0
- package/src/scripts/lint_design_quality.ts +22 -0
- package/src/scripts/lint_design_slop.ts +23 -0
- package/src/scripts/lint_discovery_manifest.ts +23 -0
- package/src/scripts/lint_discovery_vocabulary.ts +19 -0
- package/src/scripts/lint_documented_commands.ts +23 -0
- package/src/scripts/lint_empty_roadmaps.ts +23 -0
- package/src/scripts/lint_eval_fixture_citations.ts +19 -0
- package/src/scripts/lint_eval_freshness.ts +35 -0
- package/src/scripts/lint_examples.ts +18 -3
- package/src/scripts/lint_explain_trace.ts +23 -0
- package/src/scripts/lint_featured_skills.ts +26 -1
- package/src/scripts/lint_flows.ts +19 -0
- package/src/scripts/lint_frontmatter_boilerplate.ts +20 -0
- package/src/scripts/lint_ghostwriter_source.ts +26 -6
- package/src/scripts/lint_glama_drift.ts +24 -0
- package/src/scripts/lint_global_paths.ts +24 -0
- package/src/scripts/lint_governed_writes.ts +20 -0
- package/src/scripts/lint_hidden_unicode.ts +25 -0
- package/src/scripts/lint_hook_concern_budget.ts +39 -6
- package/src/scripts/lint_hook_manifest.ts +22 -0
- package/src/scripts/lint_instruction_smuggling.ts +23 -0
- package/src/scripts/lint_knowledge_scale.ts +32 -2
- package/src/scripts/lint_legal_pack.ts +31 -0
- package/src/scripts/lint_marketplace.ts +26 -0
- package/src/scripts/lint_marketplace_install_completeness.ts +23 -0
- package/src/scripts/lint_mcp_config_security.ts +24 -0
- package/src/scripts/lint_mcp_registry_manifest.ts +22 -1
- package/src/scripts/lint_memory_tripwire.ts +35 -0
- package/src/scripts/lint_missions.ts +23 -7
- package/src/scripts/lint_model_tier_coverage.ts +18 -0
- package/src/scripts/lint_namespace_collisions.ts +19 -0
- package/src/scripts/lint_no_new_atomic_commands.ts +33 -0
- package/src/scripts/lint_no_python_twin_rationale.ts +18 -0
- package/src/scripts/lint_one_off_age.ts +38 -2
- package/src/scripts/lint_orchestration_dsl.ts +23 -3
- package/src/scripts/lint_orchestrator_auto_detect.ts +19 -12
- package/src/scripts/lint_originality.ts +22 -4
- package/src/scripts/lint_originality_shingles.ts +28 -3
- package/src/scripts/lint_output_slop.ts +19 -0
- package/src/scripts/lint_override_kernel_guard.ts +30 -0
- package/src/scripts/lint_pack_first_win.ts +23 -0
- package/src/scripts/lint_pack_risk_class.ts +19 -3
- package/src/scripts/lint_persistence.ts +19 -0
- package/src/scripts/lint_persona_governance.ts +18 -3
- package/src/scripts/lint_positioning.ts +23 -0
- package/src/scripts/lint_pre_migration_refs.ts +24 -0
- package/src/scripts/lint_profile_overlay_set_only.ts +24 -0
- package/src/scripts/lint_provenance.ts +21 -0
- package/src/scripts/lint_provenance_vocabulary.ts +24 -1
- package/src/scripts/lint_readme_jargon.ts +21 -3
- package/src/scripts/lint_readme_serial_comma.ts +21 -3
- package/src/scripts/lint_readme_size.ts +21 -3
- package/src/scripts/lint_regression.ts +28 -0
- package/src/scripts/lint_roadmap_blockers.ts +43 -0
- package/src/scripts/lint_roadmap_ci_steps.ts +45 -0
- package/src/scripts/lint_roadmap_complexity.ts +47 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +23 -0
- package/src/scripts/lint_rule_interactions.ts +16 -2
- package/src/scripts/lint_rule_tiers.ts +19 -6
- package/src/scripts/lint_showcase_sessions.ts +19 -3
- package/src/scripts/lint_skill_descriptions.ts +19 -0
- package/src/scripts/lint_skill_frontmatter_safety.ts +24 -0
- package/src/scripts/lint_skill_originality.ts +18 -3
- package/src/scripts/lint_skill_scripts_readonly.ts +20 -0
- package/src/scripts/lint_skill_tools.ts +84 -145
- package/src/scripts/lint_spawn_payload.ts +27 -1
- package/src/scripts/lint_store_boundary.ts +19 -0
- package/src/scripts/lint_subagent_determinism.ts +23 -1
- package/src/scripts/lint_supported_tools_matrix.ts +18 -0
- package/src/scripts/lint_surface_matrix.ts +19 -3
- package/src/scripts/lint_ticket_buildable.ts +23 -0
- package/src/scripts/lint_topics_yaml.ts +19 -12
- package/src/scripts/lint_trigger_precision.ts +27 -0
- package/src/scripts/lint_trust_coherence.ts +21 -0
- package/src/scripts/lint_ui_stack_bundles.ts +17 -3
- package/src/scripts/lint_value_dashboard.ts +19 -3
- package/src/scripts/lint_versioned_cache.ts +19 -1
- package/src/scripts/lint_workflow_security.ts +22 -14
- package/src/scripts/pr_url_reminder_hook.ts +1 -1
- package/src/scripts/skill_collision_clusters.ts +37 -3
- package/src/scripts/skill_discovery.ts +20 -0
- package/src/scripts/skill_eval_coverage.ts +22 -0
- package/src/scripts/skill_overlap.ts +32 -6
- package/src/scripts/skill_preview.ts +31 -0
- package/src/scripts/skill_trigger_eval.ts +18 -0
- package/src/scripts/skill_usage_collect.ts +21 -0
- package/src/scripts/skill_usage_report.ts +21 -0
- package/src/scripts/sweep_dead_scan_roots.ts +11 -3
- package/src/scripts/verify_physical_move.ts +24 -0
- package/src/scripts/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -37,6 +37,7 @@ import * as fs from 'node:fs';
|
|
|
37
37
|
import * as path from 'node:path';
|
|
38
38
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
39
|
|
|
40
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
40
41
|
import { scanText, type SecretFinding } from './_lib/secret_detector.js';
|
|
41
42
|
|
|
42
43
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -253,6 +254,52 @@ function _stderr(s: string): void {
|
|
|
253
254
|
export function main(argv?: readonly string[]): number {
|
|
254
255
|
const args = parseArgs(argv ?? process.argv.slice(2));
|
|
255
256
|
const mode: Mode = args.paths.length > 0 ? 'explicit' : args.all ? 'all' : 'diff';
|
|
257
|
+
|
|
258
|
+
// Scope assertion, deliberately here and not inside `scanRepo`: that export
|
|
259
|
+
// is called directly by tests and other callers against arbitrary roots, and
|
|
260
|
+
// a secret gate is the last place to widen a throw. The cost is one extra
|
|
261
|
+
// `resolveFiles` (a `git ls-files` / `git diff`); the file reads are not
|
|
262
|
+
// repeated. Assert the RESOLVED set, before the exclude/binary filters —
|
|
263
|
+
// those legitimately drop everything (a diff touching only `tests/`), so
|
|
264
|
+
// filtering to zero is not the same signal as resolving to zero.
|
|
265
|
+
// Exit 2 (usage/env) over 1 (leak found) — a dead scope means the gate could
|
|
266
|
+
// not run. `resolveFiles` already hard-throws on an unresolvable base, so
|
|
267
|
+
// that case never arrives here as a quiet zero; a non-DeadScopeError is
|
|
268
|
+
// re-thrown untouched to preserve it.
|
|
269
|
+
try {
|
|
270
|
+
const targets = resolveFiles(REPO_ROOT, mode, { explicit: args.paths, base: args.base });
|
|
271
|
+
assertScanned({
|
|
272
|
+
gate: 'check_secret_leak',
|
|
273
|
+
scanned: targets.length,
|
|
274
|
+
units: 'candidate file(s)',
|
|
275
|
+
roots:
|
|
276
|
+
mode === 'all'
|
|
277
|
+
? ['git ls-files']
|
|
278
|
+
: mode === 'explicit'
|
|
279
|
+
? ['<explicit path arguments>']
|
|
280
|
+
: [`git diff --name-only --diff-filter=ACMR ${args.base ?? 'origin/main'}`, 'git ls-files --others --exclude-standard'],
|
|
281
|
+
// `all` and `explicit` carry no reason: zero tracked files, or zero
|
|
282
|
+
// named paths, is blindness in a repo that has content.
|
|
283
|
+
...(mode === 'diff'
|
|
284
|
+
? {
|
|
285
|
+
allowEmpty:
|
|
286
|
+
'EMPTY_VALID: shift-left mode scans what the change introduces, and a ' +
|
|
287
|
+
'branch identical to its base with no untracked files introduces nothing ' +
|
|
288
|
+
'— an absent question, not an empty corpus. Deletion test: wiping the ' +
|
|
289
|
+
'tracked tree would show up here as changed paths, and an unresolvable ' +
|
|
290
|
+
'base (the dangerous zero, which shallow CI produces) already throws in ' +
|
|
291
|
+
'resolveFiles above rather than reaching this line.',
|
|
292
|
+
}
|
|
293
|
+
: {}),
|
|
294
|
+
});
|
|
295
|
+
} catch (exc) {
|
|
296
|
+
if (exc instanceof DeadScopeError) {
|
|
297
|
+
_stderr(`❌ ${exc.message}\n`);
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
throw exc;
|
|
301
|
+
}
|
|
302
|
+
|
|
256
303
|
const hits = scanRepo(REPO_ROOT, mode, { explicit: args.paths, base: args.base });
|
|
257
304
|
|
|
258
305
|
if (args.json) {
|
|
@@ -16,6 +16,8 @@ import * as path from 'node:path';
|
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
17
|
import { parse as parseYaml } from 'yaml';
|
|
18
18
|
|
|
19
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
20
|
+
|
|
19
21
|
const _HERE = fileURLToPath(import.meta.url);
|
|
20
22
|
export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
21
23
|
const SKILLS_DIR = path.join(ROOT, 'src', 'skills');
|
|
@@ -76,8 +78,38 @@ export function findBrokenPointers(entries: SkillGaps[], root: string = ROOT): s
|
|
|
76
78
|
return errs;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/** SKILL.md files under `skillsDir` — the corpus `collectSkillGaps` reads. */
|
|
82
|
+
function countSkillFiles(skillsDir: string = SKILLS_DIR): number {
|
|
83
|
+
try {
|
|
84
|
+
return fs
|
|
85
|
+
.readdirSync(skillsDir)
|
|
86
|
+
.filter((name) => fs.existsSync(path.join(skillsDir, name, 'SKILL.md'))).length;
|
|
87
|
+
} catch {
|
|
88
|
+
// A missing root counts zero, which the caller's assertion turns into a
|
|
89
|
+
// loud failure — never a silent "no gaps to check".
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
80
95
|
const quiet = argv.includes('--quiet');
|
|
96
|
+
// The scanned unit is every SKILL.md, not the subset declaring `gaps:` —
|
|
97
|
+
// asserting on the declaration count would read a vanished skills tree as
|
|
98
|
+
// "no gaps declared" and stay green, the exact blindness this guards.
|
|
99
|
+
try {
|
|
100
|
+
assertScanned({
|
|
101
|
+
gate: 'check_skill_gaps',
|
|
102
|
+
scanned: countSkillFiles(),
|
|
103
|
+
units: 'SKILL.md file(s)',
|
|
104
|
+
roots: ['src/skills'],
|
|
105
|
+
});
|
|
106
|
+
} catch (exc) {
|
|
107
|
+
if (exc instanceof DeadScopeError) {
|
|
108
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
throw exc;
|
|
112
|
+
}
|
|
81
113
|
const entries = collectSkillGaps();
|
|
82
114
|
const errs = findBrokenPointers(entries);
|
|
83
115
|
if (errs.length > 0) {
|
|
@@ -19,6 +19,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
19
19
|
import YAML from 'yaml';
|
|
20
20
|
|
|
21
21
|
import { ROOT, iter_artefacts } from './_lib/agent_src.js';
|
|
22
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
22
23
|
import { parse_frontmatter } from './validate_frontmatter.js';
|
|
23
24
|
|
|
24
25
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -107,6 +108,24 @@ function _set_hooks_for_test(overrides: Partial<typeof _hooks>): void {
|
|
|
107
108
|
function main(): number {
|
|
108
109
|
const closure = _hooks.load_pack_closure();
|
|
109
110
|
const skills = _hooks.collect_skills();
|
|
111
|
+
// The unit is every parsed SKILL.md, not the subset declaring
|
|
112
|
+
// `requires_skills:` — over a vanished skills tree "0 composition edges"
|
|
113
|
+
// is indistinguishable from a suite that legitimately declares none.
|
|
114
|
+
try {
|
|
115
|
+
assertScanned({
|
|
116
|
+
gate: 'check_skill_requires',
|
|
117
|
+
scanned: Object.keys(skills).length,
|
|
118
|
+
units: 'SKILL.md file(s)',
|
|
119
|
+
roots: ['src/agent-src', 'src/skills', 'src/rules'],
|
|
120
|
+
});
|
|
121
|
+
} catch (exc) {
|
|
122
|
+
if (exc instanceof DeadScopeError) {
|
|
123
|
+
// 1 is this gate's only failure code (0 clean / 1 violations).
|
|
124
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
throw exc;
|
|
128
|
+
}
|
|
110
129
|
const errors: string[] = [];
|
|
111
130
|
|
|
112
131
|
for (const skill_id of Object.keys(skills).sort()) {
|
|
@@ -36,6 +36,8 @@ import * as path from 'node:path';
|
|
|
36
36
|
import process from 'node:process';
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
|
|
39
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
40
|
+
|
|
39
41
|
const _HERE = fileURLToPath(import.meta.url);
|
|
40
42
|
// parents[2] of src/scripts/<file> is the repo root.
|
|
41
43
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -142,6 +144,24 @@ function main(argv: readonly string[]): number {
|
|
|
142
144
|
return _selftest();
|
|
143
145
|
}
|
|
144
146
|
const asJson = argv.includes('--json');
|
|
147
|
+
// Scope is the allowlist itself — there is no tree to walk. An emptied
|
|
148
|
+
// AUTHORING_FILES scans nothing and reports "No retired source-of-truth
|
|
149
|
+
// pointers" forever, which is the one result this gate must never produce
|
|
150
|
+
// by accident. Exit 2 (the usage / self-test class, i.e. the gate could
|
|
151
|
+
// not run), never 1 — 1 asserts a retired pointer was actually found.
|
|
152
|
+
try {
|
|
153
|
+
assertWatchlistResolves({
|
|
154
|
+
gate: 'check_source_pointer_freshness',
|
|
155
|
+
candidates: AUTHORING_FILES,
|
|
156
|
+
repoRoot: ROOT,
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err instanceof DeadScopeError) {
|
|
160
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
145
165
|
const hits: Hit[] = [];
|
|
146
166
|
for (const rel of AUTHORING_FILES) {
|
|
147
167
|
hits.push(..._scanFile(rel));
|
|
@@ -30,6 +30,8 @@ import * as path from 'node:path';
|
|
|
30
30
|
import * as fs from 'node:fs';
|
|
31
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
32
|
|
|
33
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
34
|
+
|
|
33
35
|
const QUIET = process.argv.includes('--quiet');
|
|
34
36
|
// src/scripts/check_structural_breaking.ts → three levels up is the repo root
|
|
35
37
|
// (Python: Path(__file__).resolve().parent.parent.parent).
|
|
@@ -91,12 +93,37 @@ function main(): number {
|
|
|
91
93
|
return 0; // no trunk to diff against (e.g. shallow CI) → no-op
|
|
92
94
|
}
|
|
93
95
|
const names = _git('diff', '--name-status', `${base}...HEAD`);
|
|
94
|
-
|
|
96
|
+
const changed = _splitlines(names);
|
|
97
|
+
// Replaces the bare `names.trim() === ''` early return: a branch identical
|
|
98
|
+
// to trunk genuinely changes nothing, so zero is allowed — but it now says
|
|
99
|
+
// so through the shared primitive instead of a silent `return 0`.
|
|
100
|
+
// Exit 1 is this pinned CLI's only failure code.
|
|
101
|
+
try {
|
|
102
|
+
assertScanned({
|
|
103
|
+
gate: 'check_structural_breaking',
|
|
104
|
+
scanned: changed.length,
|
|
105
|
+
units: 'changed path(s)',
|
|
106
|
+
roots: [`git diff --name-status ${BASE_REF}...HEAD`],
|
|
107
|
+
allowEmpty:
|
|
108
|
+
'EMPTY_VALID: a branch with no diff against trunk has no structural change to ' +
|
|
109
|
+
'judge — an absent question, not an empty corpus. Deletion test: deleting the ' +
|
|
110
|
+
'watched artifact trees would show up here as D lines (the very break this gate ' +
|
|
111
|
+
'reports), so a wiped tree cannot produce this zero. An unresolvable base does ' +
|
|
112
|
+
'not reach here either — it exits at the no-trunk no-op above.',
|
|
113
|
+
});
|
|
114
|
+
} catch (exc) {
|
|
115
|
+
if (exc instanceof DeadScopeError) {
|
|
116
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
throw exc;
|
|
120
|
+
}
|
|
121
|
+
if (changed.length === 0) {
|
|
95
122
|
return 0; // nothing changed vs trunk
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
const breaks: string[] = [];
|
|
99
|
-
for (const line of
|
|
126
|
+
for (const line of changed) {
|
|
100
127
|
const parts = line.split('\t');
|
|
101
128
|
const status = parts[0]!;
|
|
102
129
|
if (status.startsWith('D') && ARTIFACT_RE.test(parts[1]!)) {
|
|
@@ -37,6 +37,8 @@ import * as path from 'node:path';
|
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
import { parse as parseYaml } from 'yaml';
|
|
39
39
|
|
|
40
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
41
|
+
|
|
40
42
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
41
43
|
const SCRIPTS = path.join(ROOT, 'src', 'scripts');
|
|
42
44
|
const REGISTRY = path.join(SCRIPTS, 'surface-tiers.yml');
|
|
@@ -105,6 +107,20 @@ function check_exhaustive(clusters: Map<string, string>): string[] {
|
|
|
105
107
|
return errors;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
/** Cluster dirs on disk under SCRIPTS — the corpus `check_exhaustive` classifies. */
|
|
111
|
+
function _count_cluster_dirs(): number {
|
|
112
|
+
let children: fs.Dirent[];
|
|
113
|
+
try {
|
|
114
|
+
children = fs.readdirSync(SCRIPTS, { withFileTypes: true });
|
|
115
|
+
} catch {
|
|
116
|
+
// A missing root counts zero; the caller's assertion makes that loud.
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
return children.filter(
|
|
120
|
+
(c) => !_IGNORE_DIRS.has(c.name) && _isDir(path.join(SCRIPTS, c.name)),
|
|
121
|
+
).length;
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
function _module_tier(
|
|
109
125
|
dotted: string,
|
|
110
126
|
clusters: Map<string, string>,
|
|
@@ -527,6 +543,24 @@ function main(argv?: readonly string[]): number {
|
|
|
527
543
|
args.skip_imports || envSkip === '1' || envSkip === 'true' || envSkip === 'yes';
|
|
528
544
|
|
|
529
545
|
const { clusters, lab_modules } = _load_registry();
|
|
546
|
+
// The unit is the cluster dirs found on disk, not `clusters.size` (the
|
|
547
|
+
// registry's own entries): over a vanished src/scripts/ every registry
|
|
548
|
+
// entry goes unchallenged and both assertions are trivially clean.
|
|
549
|
+
try {
|
|
550
|
+
assertScanned({
|
|
551
|
+
gate: 'check_surface_tiers',
|
|
552
|
+
scanned: _count_cluster_dirs(),
|
|
553
|
+
units: 'cluster dir(s)',
|
|
554
|
+
roots: ['src/scripts'],
|
|
555
|
+
});
|
|
556
|
+
} catch (exc) {
|
|
557
|
+
if (exc instanceof DeadScopeError) {
|
|
558
|
+
// 1 = violation(s); 2 is reserved for argparse usage errors.
|
|
559
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
560
|
+
return 1;
|
|
561
|
+
}
|
|
562
|
+
throw exc;
|
|
563
|
+
}
|
|
530
564
|
const errors = check_exhaustive(clusters);
|
|
531
565
|
if (skip_imports) {
|
|
532
566
|
process.stdout.write('surface-tiers: import boundary check SKIPPED (kill-switch).\n');
|
|
@@ -27,6 +27,7 @@ import * as path from 'node:path';
|
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
28
|
|
|
29
29
|
import { resolve_logical } from './_lib/agent_src.js';
|
|
30
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
30
31
|
|
|
31
32
|
const _HERE = fileURLToPath(import.meta.url);
|
|
32
33
|
// Python `Path(__file__).resolve().parents[2]` — two dirs up from src/scripts.
|
|
@@ -159,8 +160,29 @@ function main(argv: readonly string[] = []): number {
|
|
|
159
160
|
return 1;
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
// Scope is two named templates, one of them resolved through
|
|
164
|
+
// `resolve_logical` — a resolver returning a path under an artefact root
|
|
165
|
+
// that no longer exists is exactly how this comparison ends up with
|
|
166
|
+
// nothing to compare. Exit 1: it is this gate's only failure code, and it
|
|
167
|
+
// already carries the could-not-run meaning (a missing template file is
|
|
168
|
+
// reported as a failure below); 2 is reserved for CLI misuse.
|
|
169
|
+
const templates = _template_files();
|
|
170
|
+
try {
|
|
171
|
+
assertWatchlistResolves({
|
|
172
|
+
gate: 'check_template_pin_drift',
|
|
173
|
+
candidates: templates.map((p) => path.relative(REPO_ROOT, p)),
|
|
174
|
+
repoRoot: REPO_ROOT,
|
|
175
|
+
});
|
|
176
|
+
} catch (exc) {
|
|
177
|
+
if (exc instanceof DeadScopeError) {
|
|
178
|
+
process.stderr.write(`❌ check_template_pin_drift: ${exc.message}\n`);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
throw exc;
|
|
182
|
+
}
|
|
183
|
+
|
|
162
184
|
const failures: string[] = [];
|
|
163
|
-
for (const template of
|
|
185
|
+
for (const template of templates) {
|
|
164
186
|
const rel = _relToPosixOrAbs(template, REPO_ROOT);
|
|
165
187
|
if (!_isFile(template)) {
|
|
166
188
|
failures.push(`missing template file: ${rel}`);
|
|
@@ -17,6 +17,8 @@ import * as fs from 'node:fs';
|
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
19
|
|
|
20
|
+
import { assertScanned } from './_lib/scan_scope.js';
|
|
21
|
+
|
|
20
22
|
const _HERE = fileURLToPath(import.meta.url);
|
|
21
23
|
|
|
22
24
|
// Injectable for tests (monkeypatch parity): REPO_ROOT, _git_name_status,
|
|
@@ -177,7 +179,21 @@ function parse_args(argv: readonly string[]): ParsedArgs {
|
|
|
177
179
|
|
|
178
180
|
function main(argv?: readonly string[]): number {
|
|
179
181
|
const opts = parse_args(argv ?? process.argv.slice(2));
|
|
180
|
-
const
|
|
182
|
+
const base_ref = _resolve_base_ref(opts.base_ref);
|
|
183
|
+
const changed = _hooks.git_name_status(base_ref);
|
|
184
|
+
assertScanned({
|
|
185
|
+
gate: 'check_test_coverage_diff',
|
|
186
|
+
scanned: changed.length,
|
|
187
|
+
units: 'changed path(s)',
|
|
188
|
+
roots: [`git diff --name-status ${base_ref}...HEAD`],
|
|
189
|
+
allowEmpty:
|
|
190
|
+
'EMPTY_VALID: a diff with no changed paths adds no gate, so there is nothing for a ' +
|
|
191
|
+
'coverage forcing-function to warn about. Deletion test: a deleted worktree cannot ' +
|
|
192
|
+
'produce this state — an UNREADABLE range is a different case and is not silent, ' +
|
|
193
|
+
'`_git_name_status` prints `⚠️ coverage-diff: git diff failed (…); skipping.` to ' +
|
|
194
|
+
'stderr before returning empty. The exit code is 0 either way by pinned contract ' +
|
|
195
|
+
'(warn-only this phase), so this assertion declares the scope rather than gating it.',
|
|
196
|
+
});
|
|
181
197
|
const [warnings, suppressed] = evaluate(changed, _hooks.pragma_reason_from_tree);
|
|
182
198
|
if (warnings.length > 0) {
|
|
183
199
|
process.stdout.write(
|
|
@@ -20,6 +20,7 @@ import * as path from 'node:path';
|
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
21
|
|
|
22
22
|
import { resolve_logical, strip_source_prefix } from './_lib/agent_src.js';
|
|
23
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
23
24
|
|
|
24
25
|
const _HERE = fileURLToPath(import.meta.url);
|
|
25
26
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -150,9 +151,23 @@ function main(): number {
|
|
|
150
151
|
}
|
|
151
152
|
const text = fs.readFileSync(SKILL, 'utf-8');
|
|
152
153
|
const rows = parse_catalog(text);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// Supersedes the hand-rolled "no parseable catalog rows" guard: same exit
|
|
155
|
+
// code and stream, but the shared assertion names the root, so a renamed
|
|
156
|
+
// `## Catalog` heading or a moved SKILL.md reports as a dead scope instead
|
|
157
|
+
// of a bare ERROR line.
|
|
158
|
+
try {
|
|
159
|
+
assertScanned({
|
|
160
|
+
gate: 'check_token_optimizer_freshness',
|
|
161
|
+
scanned: rows.length,
|
|
162
|
+
units: 'catalog row(s)',
|
|
163
|
+
roots: [path.relative(REPO_ROOT, SKILL)],
|
|
164
|
+
});
|
|
165
|
+
} catch (exc) {
|
|
166
|
+
if (exc instanceof DeadScopeError) {
|
|
167
|
+
process.stderr.write(`ERROR: ${exc.message}\n`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
throw exc;
|
|
156
171
|
}
|
|
157
172
|
const allErrs: string[] = [];
|
|
158
173
|
let checked = 0;
|
|
@@ -43,6 +43,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
43
43
|
|
|
44
44
|
import * as yaml from 'js-yaml';
|
|
45
45
|
|
|
46
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
47
|
+
|
|
46
48
|
const _HERE = fileURLToPath(import.meta.url);
|
|
47
49
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
48
50
|
const CORPUS = path.join(REPO_ROOT, 'internal/bench/corpora/token-quality-golden.yaml');
|
|
@@ -350,6 +352,25 @@ function main(argv: string[]): number {
|
|
|
350
352
|
// Structure (incl. the fires-check) always validates against the FULL
|
|
351
353
|
// universe — a maintainer-rule tag is not "unknown" under consumer scope.
|
|
352
354
|
const report = validate(corpus, ruleIds, router);
|
|
355
|
+
// `tasks: []` parses, validates clean, and covers nothing — and without
|
|
356
|
+
// `--require-complete` an uncovered universe is only a ⚠️, so an emptied
|
|
357
|
+
// corpus exits 0 while proving nothing about any rule.
|
|
358
|
+
try {
|
|
359
|
+
assertScanned({
|
|
360
|
+
gate: 'check_token_quality_golden',
|
|
361
|
+
scanned: report.task_count,
|
|
362
|
+
units: 'golden task(s)',
|
|
363
|
+
roots: [path.relative(REPO_ROOT, CORPUS)],
|
|
364
|
+
});
|
|
365
|
+
} catch (e) {
|
|
366
|
+
if (e instanceof DeadScopeError) {
|
|
367
|
+
// 2 = invalid corpus; 1 is reserved for an unreadable file, and this one
|
|
368
|
+
// read and parsed fine.
|
|
369
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
370
|
+
return 2;
|
|
371
|
+
}
|
|
372
|
+
throw e;
|
|
373
|
+
}
|
|
353
374
|
|
|
354
375
|
// Coverage accounting runs against the SCOPED universe: which flip is
|
|
355
376
|
// being gated decides what "complete" means.
|
|
@@ -41,6 +41,8 @@ import * as path from 'node:path';
|
|
|
41
41
|
import process from 'node:process';
|
|
42
42
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
43
43
|
|
|
44
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
45
|
+
|
|
44
46
|
const _HERE = fileURLToPath(import.meta.url);
|
|
45
47
|
// REPO_ROOT = <repo>/src/scripts/<file> → up two.
|
|
46
48
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -216,6 +218,29 @@ function main(argv: string[]): number {
|
|
|
216
218
|
}
|
|
217
219
|
const current = extract_metrics(report);
|
|
218
220
|
|
|
221
|
+
// `_num` returns 0 both for "absent from the report" and for "genuinely
|
|
222
|
+
// zero", and no gated surface is ever legitimately zero tokens — so a
|
|
223
|
+
// renamed report key reads as 0, every pct goes negative against the
|
|
224
|
+
// baseline, and the gate reports ✅ while measuring nothing. Counting the
|
|
225
|
+
// metrics that resolved is counting units READ, not units passing. Asserted
|
|
226
|
+
// BEFORE --update-baseline so a report of zeros can never be recorded as the
|
|
227
|
+
// baseline every later run compares against. Exit 1 (the file/argument
|
|
228
|
+
// error class — the gate could not run); 2 means a real regression.
|
|
229
|
+
try {
|
|
230
|
+
assertScanned({
|
|
231
|
+
gate: 'check_token_regression',
|
|
232
|
+
scanned: Object.values(current.metrics).filter((v) => v > 0).length,
|
|
233
|
+
units: 'gated metric(s)',
|
|
234
|
+
roots: [path.relative(REPO_ROOT, PROJECTION_REPORT)],
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err instanceof DeadScopeError) {
|
|
238
|
+
process.stderr.write(`❌ ${err.message}\n`);
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
|
|
219
244
|
if (updateBaseline) {
|
|
220
245
|
const payload = JSON.stringify(current, null, 2) + '\n';
|
|
221
246
|
fs.writeFileSync(BASELINE_FILE, payload);
|
|
@@ -19,14 +19,23 @@ import * as fs from 'node:fs';
|
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
|
|
22
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
23
|
+
|
|
22
24
|
const _HERE = fileURLToPath(import.meta.url);
|
|
23
25
|
|
|
24
26
|
function main(argv?: readonly string[]): number {
|
|
25
27
|
const args = argv ?? process.argv.slice(2);
|
|
26
28
|
const quiet = args.includes('--quiet');
|
|
27
29
|
|
|
30
|
+
let tracked: string;
|
|
28
31
|
let output: string;
|
|
29
32
|
try {
|
|
33
|
+
// The corpus is the tracked index, not the `-ci` result — that result IS
|
|
34
|
+
// the finding list, so counting it would count violations. Read the index
|
|
35
|
+
// itself: `git ls-files` is path-scoped to the cwd, so running from a
|
|
36
|
+
// subtree with nothing tracked yields an empty index AND an empty `-ci`
|
|
37
|
+
// result, and the gate prints its green line having examined nothing.
|
|
38
|
+
tracked = execSync('git ls-files', { encoding: 'utf-8', cwd: process.cwd() }).trim();
|
|
30
39
|
output = execSync('git ls-files -ci --exclude-standard', {
|
|
31
40
|
encoding: 'utf-8',
|
|
32
41
|
cwd: process.cwd(),
|
|
@@ -36,6 +45,23 @@ function main(argv?: readonly string[]): number {
|
|
|
36
45
|
return 2;
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
try {
|
|
49
|
+
assertScanned({
|
|
50
|
+
gate: 'check_tracked_but_ignored',
|
|
51
|
+
scanned: tracked ? tracked.split('\n').filter(Boolean).length : 0,
|
|
52
|
+
units: 'tracked file(s)',
|
|
53
|
+
roots: ['git ls-files (cwd-scoped index)'],
|
|
54
|
+
});
|
|
55
|
+
} catch (exc) {
|
|
56
|
+
if (exc instanceof DeadScopeError) {
|
|
57
|
+
// 2 = "this gate could not run" (its git-unavailable code), never 1,
|
|
58
|
+
// which asserts the stronger claim that ignored-but-tracked files exist.
|
|
59
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
throw exc;
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
if (!output) {
|
|
40
66
|
if (!quiet) {
|
|
41
67
|
process.stdout.write(`✅ No tracked-but-ignored files found.\n`);
|
|
@@ -26,6 +26,8 @@ import * as fs from 'node:fs';
|
|
|
26
26
|
import * as path from 'node:path';
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
28
|
|
|
29
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
30
|
+
|
|
29
31
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
30
32
|
const SKILLS_DIR = path.join(REPO_ROOT, 'src', 'skills');
|
|
31
33
|
const ALLOWLIST_PATH = path.join(REPO_ROOT, 'src', 'scripts', 'trigger_eval_grandfather.json');
|
|
@@ -128,7 +130,27 @@ function main(): number {
|
|
|
128
130
|
return 2;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
// The ratchet only bites per skill directory found: an empty (or moved)
|
|
134
|
+
// skills root makes every rule in `check` iterate nothing, and the ✅ line
|
|
135
|
+
// reads "0/0 skills carry evals/triggers.json" — a passing presence gate
|
|
136
|
+
// over no skills at all.
|
|
137
|
+
const skills = list_skill_dirs(SKILLS_DIR);
|
|
138
|
+
try {
|
|
139
|
+
assertScanned({
|
|
140
|
+
gate: 'check_trigger_eval_presence',
|
|
141
|
+
scanned: skills.length,
|
|
142
|
+
units: 'skill director(ies)',
|
|
143
|
+
roots: ['src/skills'],
|
|
144
|
+
});
|
|
145
|
+
} catch (exc) {
|
|
146
|
+
if (exc instanceof DeadScopeError) {
|
|
147
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
throw exc;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { errors, covered, total } = check(skills, allowlist.skills);
|
|
132
154
|
|
|
133
155
|
if (errors.length) {
|
|
134
156
|
process.stderr.write('❌ check-trigger-eval-presence: presence-ratchet violation(s):\n');
|
|
@@ -27,6 +27,8 @@ import * as fs from 'node:fs';
|
|
|
27
27
|
import * as path from 'node:path';
|
|
28
28
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
29
|
|
|
30
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
31
|
+
|
|
30
32
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
31
33
|
const GLOB = 'src/skills/*/evals/triggers.json';
|
|
32
34
|
const MAX_AGE_DAYS = 90;
|
|
@@ -276,6 +278,20 @@ function _isFile(p: string): boolean {
|
|
|
276
278
|
}
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
/** Immediate `src/skills/*` directories — the population `_glob_triggers` filters. */
|
|
282
|
+
function _skill_dirs(root: string): string[] {
|
|
283
|
+
const skillsDir = path.join(root, 'src', 'skills');
|
|
284
|
+
let entries: fs.Dirent[];
|
|
285
|
+
try {
|
|
286
|
+
entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
287
|
+
} catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
return entries
|
|
291
|
+
.filter((ent) => ent.isDirectory() || ent.isSymbolicLink())
|
|
292
|
+
.map((ent) => path.join(skillsDir, ent.name));
|
|
293
|
+
}
|
|
294
|
+
|
|
279
295
|
/** Component-wise path comparison matching PosixPath ordering. */
|
|
280
296
|
function _pathSort(a: string, b: string): number {
|
|
281
297
|
const pa = a.split(path.sep);
|
|
@@ -342,6 +358,26 @@ function main(): number {
|
|
|
342
358
|
return 2;
|
|
343
359
|
}
|
|
344
360
|
|
|
361
|
+
// The scanned unit is the skill directory, not the triggers.json subset:
|
|
362
|
+
// whether a given skill ships evals is `check_trigger_eval_presence`'s
|
|
363
|
+
// ratchet, so zero trigger sets under a populated tree is that gate's
|
|
364
|
+
// finding, not this one's. Zero skill DIRECTORIES is this gate's blindness
|
|
365
|
+
// — the ✅ line then reads "0 trigger set(s) fresh + valid".
|
|
366
|
+
try {
|
|
367
|
+
assertScanned({
|
|
368
|
+
gate: 'check_trigger_evals',
|
|
369
|
+
scanned: _skill_dirs(REPO_ROOT).length,
|
|
370
|
+
units: 'skill director(ies)',
|
|
371
|
+
roots: ['src/skills'],
|
|
372
|
+
});
|
|
373
|
+
} catch (exc) {
|
|
374
|
+
if (exc instanceof DeadScopeError) {
|
|
375
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
376
|
+
return 1;
|
|
377
|
+
}
|
|
378
|
+
throw exc;
|
|
379
|
+
}
|
|
380
|
+
|
|
345
381
|
const files = _glob_triggers(REPO_ROOT);
|
|
346
382
|
const errors: string[] = [];
|
|
347
383
|
for (const p of files) {
|
|
@@ -16,6 +16,8 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
|
|
19
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
20
|
+
|
|
19
21
|
export type DriftInput = {
|
|
20
22
|
/** True when origin/main is an ancestor of HEAD (branch contains trunk tip). */
|
|
21
23
|
trunkIsAncestor: boolean;
|
|
@@ -42,8 +44,13 @@ function _git(args: string[]): string {
|
|
|
42
44
|
|
|
43
45
|
export function main(): number {
|
|
44
46
|
let input: DriftInput;
|
|
47
|
+
let trunkCommits = 0;
|
|
45
48
|
try {
|
|
46
49
|
_git(['fetch', 'origin', 'main', '--quiet']);
|
|
50
|
+
// The comparison is only meaningful against a trunk that has history.
|
|
51
|
+
// An origin/main pointing at an empty/orphan ref answers "0 behind" and
|
|
52
|
+
// scores the branch up to date without ever having read a trunk commit.
|
|
53
|
+
trunkCommits = Number(_git(['rev-list', '--count', 'origin/main']) || '0');
|
|
47
54
|
let isAncestor = true;
|
|
48
55
|
try {
|
|
49
56
|
execFileSync('git', ['merge-base', '--is-ancestor', 'origin/main', 'HEAD'], { stdio: 'ignore' });
|
|
@@ -56,6 +63,22 @@ export function main(): number {
|
|
|
56
63
|
process.stdout.write(`⚠️ trunk-drift check skipped (no origin/main or git error): ${String(e)}\n`);
|
|
57
64
|
return 0; // advisory — never block on a missing remote
|
|
58
65
|
}
|
|
66
|
+
try {
|
|
67
|
+
assertScanned({
|
|
68
|
+
gate: 'check_trunk_drift',
|
|
69
|
+
scanned: trunkCommits,
|
|
70
|
+
units: 'trunk commit(s)',
|
|
71
|
+
roots: ['origin/main'],
|
|
72
|
+
});
|
|
73
|
+
} catch (exc) {
|
|
74
|
+
if (exc instanceof DeadScopeError) {
|
|
75
|
+
// 1 is the only failure code; 0 means pass/advisory-skip, so a dead
|
|
76
|
+
// scope must not borrow it — a gate that read no trunk has not passed.
|
|
77
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
throw exc;
|
|
81
|
+
}
|
|
59
82
|
const v = driftVerdict(input);
|
|
60
83
|
process.stdout.write(`${v.message}\n`);
|
|
61
84
|
return v.ok ? 0 : 1;
|