@event4u/agent-config 9.20.0 → 9.22.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 +86 -0
- package/dist/agent-src/commands/worktree/cleanup.md +17 -0
- package/dist/agent-src/commands/worktree/status.md +6 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +22 -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 +5 -5
- 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 +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- 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/bench_ab_activation.ts +368 -0
- 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/bench_ab_task_runner.ts +18 -0
- package/src/scripts/bench_ab_v2_run.ts +147 -6
- package/src/scripts/bench_ab_v2_stats.ts +74 -2
- 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/worktree_cleanup_check.ts +319 -4
- package/src/scripts/check_bite_sized_granularity.ts +0 -116
- package/src/scripts/lint_workspace_boundary.ts +0 -267
|
@@ -22,6 +22,8 @@ import * as fs from 'node:fs';
|
|
|
22
22
|
import * as path from 'node:path';
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
24
|
|
|
25
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
26
|
+
|
|
25
27
|
const _HERE = fileURLToPath(import.meta.url);
|
|
26
28
|
// src/scripts/lint_showcase_sessions.ts → parent.parent.parent is the repo root.
|
|
27
29
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -174,9 +176,23 @@ function _validate_session(slug: string): string[] {
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
function main(): number {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
// There is no corpus to count — "0 referenced, 0 on disk" is the shipped
|
|
180
|
+
// state and a legitimate pass. The one thing that must resolve is the
|
|
181
|
+
// manifest both rules are derived from: with showcase.md gone there are no
|
|
182
|
+
// references, so no missing file and no orphan can ever be reported.
|
|
183
|
+
// Supersedes the previous bare `_isFile` check, same exit code.
|
|
184
|
+
try {
|
|
185
|
+
assertWatchlistResolves({
|
|
186
|
+
gate: 'lint_showcase_sessions',
|
|
187
|
+
candidates: [_relTo(SHOWCASE_MD, ROOT)],
|
|
188
|
+
repoRoot: ROOT,
|
|
189
|
+
});
|
|
190
|
+
} catch (e) {
|
|
191
|
+
if (e instanceof DeadScopeError) {
|
|
192
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
throw e;
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
const text = fs.readFileSync(SHOWCASE_MD, 'utf-8');
|
|
@@ -61,6 +61,7 @@ import process from 'node:process';
|
|
|
61
61
|
import { fileURLToPath } from 'node:url';
|
|
62
62
|
|
|
63
63
|
import { OVERLAP_THRESHOLD, _cosine, collect } from './audit_skill_overlap.js';
|
|
64
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
64
65
|
import { parse_frontmatter } from './skill_overlap.js';
|
|
65
66
|
|
|
66
67
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -289,6 +290,24 @@ function main(argv: string[]): number {
|
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
|
|
293
|
+
// `skillMds` yields nothing for a root that does not exist, so a moved
|
|
294
|
+
// `src/skills` would report "no description defects" over zero skills —
|
|
295
|
+
// routing quality certified against an empty corpus.
|
|
296
|
+
try {
|
|
297
|
+
assertScanned({
|
|
298
|
+
gate: 'lint_skill_descriptions',
|
|
299
|
+
scanned,
|
|
300
|
+
units: 'skill(s)',
|
|
301
|
+
roots: [path.relative(REPO, args.root) || args.root],
|
|
302
|
+
});
|
|
303
|
+
} catch (exc) {
|
|
304
|
+
if (exc instanceof DeadScopeError) {
|
|
305
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
throw exc;
|
|
309
|
+
}
|
|
310
|
+
|
|
292
311
|
if (args.json) {
|
|
293
312
|
fs.mkdirSync(path.dirname(args.json), { recursive: true });
|
|
294
313
|
fs.writeFileSync(args.json, JSON.stringify({ scanned, violations }, null, 2) + '\n');
|
|
@@ -27,6 +27,7 @@ import * as fs from 'node:fs';
|
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
28
|
|
|
29
29
|
import * as sl from './_lib/security_lint.js';
|
|
30
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
30
31
|
|
|
31
32
|
export const CHECK = 'dangerous-frontmatter';
|
|
32
33
|
|
|
@@ -258,12 +259,35 @@ export function main(argv: readonly string[] | null = null): number {
|
|
|
258
259
|
|
|
259
260
|
const findings: sl.Finding[] = [];
|
|
260
261
|
const roots = ['src/skills', 'src/agent-src', 'src/domains'];
|
|
262
|
+
let scanned = 0;
|
|
261
263
|
for (const sf of sl.iter_corpus(roots, ['.md'])) {
|
|
264
|
+
scanned += 1;
|
|
262
265
|
for (const h of _scan(sf)) {
|
|
263
266
|
findings.push(h);
|
|
264
267
|
}
|
|
265
268
|
}
|
|
266
269
|
|
|
270
|
+
// `iter_corpus` skips a root that does not exist, so all three moving at
|
|
271
|
+
// once yields zero files and a clean report — the quietest possible failure
|
|
272
|
+
// for a security lint. `scanned` counts files READ, never findings: zero
|
|
273
|
+
// findings over 900 files is the success state, zero files is blindness.
|
|
274
|
+
// Exit 1 = "this gate is not clean" (reds the build); 2 stays the argv
|
|
275
|
+
// usage code that `parse_args` owns.
|
|
276
|
+
try {
|
|
277
|
+
assertScanned({
|
|
278
|
+
gate: 'lint_skill_frontmatter_safety',
|
|
279
|
+
scanned,
|
|
280
|
+
units: 'markdown file(s)',
|
|
281
|
+
roots,
|
|
282
|
+
});
|
|
283
|
+
} catch (exc) {
|
|
284
|
+
if (exc instanceof DeadScopeError) {
|
|
285
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
throw exc;
|
|
289
|
+
}
|
|
290
|
+
|
|
267
291
|
if (args.json) {
|
|
268
292
|
const payload = sl.py_json_dumps_indent2(findings.map((f) => f.toDict()));
|
|
269
293
|
process.stdout.write(payload + '\n');
|
|
@@ -55,6 +55,7 @@ import * as path from 'node:path';
|
|
|
55
55
|
import process from 'node:process';
|
|
56
56
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
57
57
|
|
|
58
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
58
59
|
import { jaccard, parse_frontmatter, tokenize } from './skill_overlap.js';
|
|
59
60
|
|
|
60
61
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -257,9 +258,23 @@ function main(argv?: readonly string[]): number {
|
|
|
257
258
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
258
259
|
|
|
259
260
|
const skills = load_skills(SKILLS);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
try {
|
|
262
|
+
assertScanned({
|
|
263
|
+
gate: 'lint_skill_originality',
|
|
264
|
+
scanned: skills.length,
|
|
265
|
+
units: 'skill(s)',
|
|
266
|
+
roots: ['src/skills'],
|
|
267
|
+
});
|
|
268
|
+
} catch (exc) {
|
|
269
|
+
if (exc instanceof DeadScopeError) {
|
|
270
|
+
// The `no skills under <root>` line is part of the pinned CLI
|
|
271
|
+
// contract, so it stays first; the assertion's named-root detail
|
|
272
|
+
// follows it rather than replacing it.
|
|
273
|
+
process.stderr.write(`no skills under ${SKILLS}\n`);
|
|
274
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
throw exc;
|
|
263
278
|
}
|
|
264
279
|
const allow = load_allowlist();
|
|
265
280
|
const findings = analyse(skills, allow);
|
|
@@ -28,6 +28,8 @@ import * as path from 'node:path';
|
|
|
28
28
|
import process from 'node:process';
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
|
|
31
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
32
|
+
|
|
31
33
|
const _HERE = fileURLToPath(import.meta.url);
|
|
32
34
|
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
33
35
|
const DEFAULT_SKILLS = path.join(REPO, 'src', 'skills');
|
|
@@ -127,6 +129,24 @@ function main(argv: string[]): number {
|
|
|
127
129
|
violations.push({ rel, detail: 'contains a content-mutation primitive but no write-gating flag (--writable/--apply/--write/--out/--fix) and is not allowlisted' });
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
// `scanned` is the script files read, which is also the number the success
|
|
133
|
+
// line publishes; a `--root` that resolves to nothing would otherwise
|
|
134
|
+
// report every script gated while having opened none.
|
|
135
|
+
try {
|
|
136
|
+
assertScanned({
|
|
137
|
+
gate: 'lint_skill_scripts_readonly',
|
|
138
|
+
scanned,
|
|
139
|
+
units: 'skill script(s)',
|
|
140
|
+
roots: [args.root === DEFAULT_SKILLS ? 'src/skills' : args.root],
|
|
141
|
+
});
|
|
142
|
+
} catch (exc) {
|
|
143
|
+
if (exc instanceof DeadScopeError) {
|
|
144
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
throw exc;
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
if (violations.length > 0) {
|
|
131
151
|
process.stderr.write(`❌ lint_skill_scripts_readonly: ${violations.length} ungated write(s):\n`);
|
|
132
152
|
for (const v of violations) process.stderr.write(` ${v.rel} — ${v.detail}\n`);
|
|
@@ -1,161 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
/**
|
|
3
|
-
* Block D · D1 meta-linter for `scripts/skill_tools/*.
|
|
3
|
+
* Block D · D1 meta-linter for `src/scripts/skill_tools/*.ts` (ADR-006).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* Phase 4 / Wave 4b). The CLI contract is pinned — `--json`
|
|
7
|
-
* / `--quiet` / `--tools-dir` flags, exit codes (0 clean, 1 violations,
|
|
8
|
-
* 2 usage / missing-dir), stdout split (JSON or human), byte-identical
|
|
9
|
-
* violation strings, same glob + sort order, same `_SAMPLE`/`__main__`
|
|
10
|
-
* detection.
|
|
5
|
+
* ## Why this gate changed shape (2026-08-05)
|
|
11
6
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* `ast` walk avoids — flagged as a divergence candidate, not observed in
|
|
19
|
-
* the corpus.
|
|
7
|
+
* ADR-006 piloted these tools in Python and this gate enforced five invariants
|
|
8
|
+
* over `skill_tools/*.py`. ADR-200 migrated the corpus to `.ts` without
|
|
9
|
+
* mentioning ADR-006, so the glob matched nothing: for ~7 weeks the gate printed
|
|
10
|
+
* `✅ scripts/skill_tools/ — all tools clean.` over **zero** tools on every CI
|
|
11
|
+
* run, and nobody noticed that two ADR-006 invariants had quietly gone unmet on
|
|
12
|
+
* the TS surface (`--help` in 0 of 6 files; `_SAMPLE` in 3 of 5).
|
|
20
13
|
*
|
|
21
|
-
*
|
|
14
|
+
* Repointing the glob and keeping all five checks was measured and rejected: the
|
|
15
|
+
* TS ports are 2.4–4.0× their Python originals (92–142 LOC → 253–470), so the
|
|
16
|
+
* 200-LOC cap reds 5 of 5 tools, and the only way to green it is raising the cap
|
|
17
|
+
* to ≥ 470 — threshold-lowering, which this gate's own ratchet exists to
|
|
18
|
+
* prevent. AI council 2026-08-05 resolved it as extract-the-checkable:
|
|
19
|
+
*
|
|
20
|
+
* - **KEPT** (pure regex over filename + text, no analyser): `snake_case_verb_noun`
|
|
21
|
+
* naming, a registered `--json` flag, and an embedded `_SAMPLE` or CLI-entry guard.
|
|
22
|
+
* - **DROPPED, recorded in ADR-006's Status**: the 200-LOC cap (cannot pass
|
|
23
|
+
* without weakening its own threshold); the `argparse` import and
|
|
24
|
+
* `add_help=False` checks (no TypeScript analogue); and the stdlib-only import
|
|
25
|
+
* scan (a Python `ast`-shaped scanner — re-implementing it for TS specifiers
|
|
26
|
+
* would be a new analyser, not a port, and the council drew that line
|
|
27
|
+
* explicitly for the sibling `lint_workspace_boundary` decision).
|
|
28
|
+
*
|
|
29
|
+
* The gate now asserts its scan scope, so an emptied or moved corpus fails
|
|
30
|
+
* loudly instead of certifying nothing. No `allowEmpty`: "no tools" must never
|
|
31
|
+
* read as "all tools clean" again.
|
|
32
|
+
*
|
|
33
|
+
* CLI contract unchanged — `--json` / `--quiet` / `--tools-dir`, exit codes
|
|
34
|
+
* (0 clean, 1 violations, 2 usage / missing-dir / dead scope), stdout split.
|
|
22
35
|
*/
|
|
23
36
|
|
|
24
37
|
import * as fs from 'node:fs';
|
|
25
38
|
import * as path from 'node:path';
|
|
26
39
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
40
|
|
|
41
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
42
|
+
|
|
28
43
|
const _HERE = fileURLToPath(import.meta.url);
|
|
29
44
|
// src/scripts/lint_skill_tools.ts → parents[2] is the repo root.
|
|
30
45
|
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
31
46
|
const TOOLS_DIR = path.join(ROOT, 'src', 'scripts', 'skill_tools');
|
|
32
|
-
const NAME_RE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+\.
|
|
33
|
-
const SIZE_CAP = 200;
|
|
34
|
-
|
|
35
|
-
const STDLIB: ReadonlySet<string> = new Set([
|
|
36
|
-
'__future__', 'abc', 'argparse', 'ast', 'base64', 'collections', 'configparser',
|
|
37
|
-
'contextlib', 'copy', 'csv', 'dataclasses', 'datetime', 'decimal', 'difflib',
|
|
38
|
-
'enum', 'errno', 'fnmatch', 'functools', 'glob', 'gzip', 'hashlib', 'heapq',
|
|
39
|
-
'html', 'http', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json',
|
|
40
|
-
'logging', 'math', 'mimetypes', 'os', 'pathlib', 'pickle', 'platform', 'posixpath',
|
|
41
|
-
'pprint', 'queue', 'random', 're', 'shlex', 'shutil', 'signal', 'socket',
|
|
42
|
-
'sqlite3', 'ssl', 'stat', 'string', 'struct', 'subprocess', 'sys', 'tempfile',
|
|
43
|
-
'textwrap', 'threading', 'time', 'tomllib', 'traceback', 'types', 'typing',
|
|
44
|
-
'unicodedata', 'unittest', 'urllib', 'uuid', 'venv', 'warnings', 'weakref',
|
|
45
|
-
'xml', 'zipfile', 'zlib',
|
|
46
|
-
]);
|
|
47
|
-
const PROJECT_PACKAGES: ReadonlySet<string> = new Set(['scripts', 'skill_tools']);
|
|
48
|
-
|
|
49
|
-
interface ParsedImport {
|
|
50
|
-
kind: 'import';
|
|
51
|
-
modules: string[]; // dotted module names from `import a, b.c as d`
|
|
52
|
-
}
|
|
53
|
-
interface ParsedFromImport {
|
|
54
|
-
kind: 'from';
|
|
55
|
-
module: string | null; // None for `from . import x` (level > 0)
|
|
56
|
-
level: number;
|
|
57
|
-
}
|
|
58
|
-
type ImportNode = ParsedImport | ParsedFromImport;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Scan a Python source for top-of-line `import` / `from … import` statements.
|
|
62
|
-
* Mirrors what `ast.walk` surfaces for `Import` / `ImportFrom` nodes (the
|
|
63
|
-
* linter only reads `.names`, `.module`, `.level`). Continuation lines inside
|
|
64
|
-
* a parenthesised `from x import (...)` do not affect the module name (which
|
|
65
|
-
* sits before `import` on the opening line), so they need no special handling.
|
|
66
|
-
*/
|
|
67
|
-
function _parse_imports(text: string): ImportNode[] {
|
|
68
|
-
const out: ImportNode[] = [];
|
|
69
|
-
for (const raw of text.split('\n')) {
|
|
70
|
-
const stripped = raw.replace(/^[ \t]+/, '');
|
|
71
|
-
const importMatch = /^import\s+(.+)$/.exec(stripped);
|
|
72
|
-
if (importMatch) {
|
|
73
|
-
const rest = importMatch[1]!.replace(/#.*$/, '');
|
|
74
|
-
const modules: string[] = [];
|
|
75
|
-
for (const part of rest.split(',')) {
|
|
76
|
-
// `a.b.c as d` → dotted name is the token before `as`.
|
|
77
|
-
const name = part.trim().split(/\s+as\s+/)[0]!.trim();
|
|
78
|
-
if (name) {
|
|
79
|
-
modules.push(name);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
out.push({ kind: 'import', modules });
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const fromMatch = /^from\s+(\.*)([A-Za-z0-9_.]*)\s+import\b/.exec(stripped);
|
|
86
|
-
if (fromMatch) {
|
|
87
|
-
const dots = fromMatch[1]!;
|
|
88
|
-
const mod = fromMatch[2]!;
|
|
89
|
-
const level = dots.length;
|
|
90
|
-
// ast: `from . import x` → module is None when no dotted name follows.
|
|
91
|
-
const module = mod === '' ? null : mod;
|
|
92
|
-
out.push({ kind: 'from', module, level });
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return out;
|
|
97
|
-
}
|
|
47
|
+
const NAME_RE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+\.ts$/;
|
|
98
48
|
|
|
99
49
|
function _violations_for(p: string): string[] {
|
|
100
50
|
const out: string[] = [];
|
|
101
51
|
const name = path.basename(p);
|
|
102
52
|
if (!NAME_RE.test(name)) {
|
|
103
|
-
out.push(`naming: \`${name}\` is not snake_case_verb_noun.
|
|
53
|
+
out.push(`naming: \`${name}\` is not snake_case_verb_noun.ts`);
|
|
104
54
|
}
|
|
105
55
|
|
|
106
56
|
const text = fs.readFileSync(p, 'utf-8');
|
|
107
|
-
let loc = 0;
|
|
108
|
-
for (const ln of text.split('\n')) {
|
|
109
|
-
if (ln.trim() && !ln.replace(/^[ \t]+/, '').startsWith('#')) {
|
|
110
|
-
loc += 1;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (loc > SIZE_CAP) {
|
|
114
|
-
out.push(`size: ${loc} LOC > ${SIZE_CAP} cap`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Imports — flag any non-stdlib, non-project top-level module + record argparse.
|
|
118
|
-
const imported = new Set<string>();
|
|
119
|
-
for (const node of _parse_imports(text)) {
|
|
120
|
-
if (node.kind === 'import') {
|
|
121
|
-
for (const dotted of node.modules) {
|
|
122
|
-
const root = dotted.split('.')[0]!;
|
|
123
|
-
imported.add(root);
|
|
124
|
-
if (!STDLIB.has(root) && !PROJECT_PACKAGES.has(root)) {
|
|
125
|
-
out.push(`stdlib-only: imports \`${dotted}\` (third-party)`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
if (node.module === null || node.level > 0) {
|
|
130
|
-
continue; // relative imports — package-internal
|
|
131
|
-
}
|
|
132
|
-
const root = node.module.split('.')[0]!;
|
|
133
|
-
imported.add(root);
|
|
134
|
-
if (!STDLIB.has(root) && !PROJECT_PACKAGES.has(root)) {
|
|
135
|
-
out.push(`stdlib-only: imports from \`${node.module}\` (third-party)`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
57
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
const hasJsonFlag = /['"]--json['"]/.test(text);
|
|
143
|
-
if (!hasArgparse) {
|
|
144
|
-
out.push('cli: no `argparse` import detected');
|
|
145
|
-
}
|
|
146
|
-
if (!hasJsonFlag) {
|
|
58
|
+
// Machine-readable output — the tool must register a `--json` flag.
|
|
59
|
+
if (!/['"]--json['"]/.test(text)) {
|
|
147
60
|
out.push('cli: missing `--json` flag');
|
|
148
61
|
}
|
|
149
|
-
if (/add_help\s*=\s*False/.test(text)) {
|
|
150
|
-
out.push('cli: `add_help=False` disables --help');
|
|
151
|
-
}
|
|
152
62
|
|
|
153
|
-
// Embedded sample data — `_SAMPLE` constant or
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
if (!(hasSample ||
|
|
158
|
-
out.push('sample: no `_SAMPLE` constant or
|
|
63
|
+
// Embedded sample data — an exported-or-bare `_SAMPLE` constant, or the
|
|
64
|
+
// CLI-entry guard that replaced Python's `__main__` block.
|
|
65
|
+
const hasSample = /^\s*(?:export\s+)?(?:const|let)?\s*_SAMPLE\s*[:=]/m.test(text);
|
|
66
|
+
const hasEntry = text.includes('import.meta.url');
|
|
67
|
+
if (!(hasSample || hasEntry)) {
|
|
68
|
+
out.push('sample: no `_SAMPLE` constant or CLI-entry guard');
|
|
159
69
|
}
|
|
160
70
|
|
|
161
71
|
return out;
|
|
@@ -191,8 +101,8 @@ function lint(toolsDir: string): [number, Record<string, string[]>] {
|
|
|
191
101
|
} catch {
|
|
192
102
|
entries = [];
|
|
193
103
|
}
|
|
194
|
-
const
|
|
195
|
-
.filter((n) => n.endsWith('.
|
|
104
|
+
const toolFiles = entries
|
|
105
|
+
.filter((n) => n.endsWith('.ts') && !n.endsWith('.d.ts') && !n.endsWith('.test.ts'))
|
|
196
106
|
.map((n) => path.join(resolved, n))
|
|
197
107
|
.filter((p) => {
|
|
198
108
|
try {
|
|
@@ -202,10 +112,14 @@ function lint(toolsDir: string): [number, Record<string, string[]>] {
|
|
|
202
112
|
}
|
|
203
113
|
})
|
|
204
114
|
.sort();
|
|
205
|
-
|
|
206
|
-
|
|
115
|
+
let linted = 0;
|
|
116
|
+
for (const p of toolFiles) {
|
|
117
|
+
// `index.ts` is the package marker — the `__init__.py` analogue. It is a
|
|
118
|
+
// re-export barrel, not a tool, so the per-tool invariants do not apply.
|
|
119
|
+
if (path.basename(p) === 'index.ts') {
|
|
207
120
|
continue;
|
|
208
121
|
}
|
|
122
|
+
linted += 1;
|
|
209
123
|
const viols = _violations_for(p);
|
|
210
124
|
if (viols.length > 0) {
|
|
211
125
|
// Mirror Python's `str(path.relative_to(ROOT))` with ValueError fallback.
|
|
@@ -213,9 +127,24 @@ function lint(toolsDir: string): [number, Record<string, string[]>] {
|
|
|
213
127
|
findings[key] = viols;
|
|
214
128
|
}
|
|
215
129
|
}
|
|
130
|
+
_lintedCount = linted;
|
|
216
131
|
return [Object.keys(findings).length > 0 ? 1 : 0, findings];
|
|
217
132
|
}
|
|
218
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Tools actually inspected by the last `lint()` call.
|
|
136
|
+
*
|
|
137
|
+
* Kept out of `lint()`'s return value on purpose: 13 unit tests drive `lint()`
|
|
138
|
+
* directly and assert its `[code, findings]` shape, and the scan-scope assertion
|
|
139
|
+
* must fire once per process in `main()` — not inside a function the tests call
|
|
140
|
+
* with temp dirs, where an empty fixture is legitimate.
|
|
141
|
+
*/
|
|
142
|
+
let _lintedCount = 0;
|
|
143
|
+
|
|
144
|
+
export function lintedCount(): number {
|
|
145
|
+
return _lintedCount;
|
|
146
|
+
}
|
|
147
|
+
|
|
219
148
|
function _print_human(findings: Record<string, string[]>): void {
|
|
220
149
|
if (Object.keys(findings).length === 0) {
|
|
221
150
|
process.stdout.write('✅ scripts/skill_tools/ — all tools clean.\n');
|
|
@@ -292,6 +221,25 @@ function main(argv?: readonly string[]): number {
|
|
|
292
221
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
293
222
|
|
|
294
223
|
const [code, findings] = lint(args.toolsDir);
|
|
224
|
+
// Scan-scope assertion. Deliberately after `lint()` (which computes the
|
|
225
|
+
// count) and before the report, so a moved or emptied corpus fails loudly
|
|
226
|
+
// instead of printing "all tools clean" over zero tools — which is exactly
|
|
227
|
+
// what this gate did for ~7 weeks after ADR-200 deleted its `.py` corpus.
|
|
228
|
+
// No `allowEmpty`: an empty tools dir here is blindness, not success.
|
|
229
|
+
try {
|
|
230
|
+
assertScanned({
|
|
231
|
+
gate: 'lint_skill_tools',
|
|
232
|
+
scanned: lintedCount(),
|
|
233
|
+
units: 'tool(s)',
|
|
234
|
+
roots: ['src/scripts/skill_tools'],
|
|
235
|
+
});
|
|
236
|
+
} catch (e) {
|
|
237
|
+
if (e instanceof DeadScopeError) {
|
|
238
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
239
|
+
return 2; // the gate's existing "internal / usage" code
|
|
240
|
+
}
|
|
241
|
+
throw e;
|
|
242
|
+
}
|
|
295
243
|
if (args.json) {
|
|
296
244
|
process.stdout.write(_json_dumps_ascii({ exit_code: code, findings }));
|
|
297
245
|
process.stdout.write('\n');
|
|
@@ -327,13 +275,4 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
327
275
|
process.exit(main());
|
|
328
276
|
}
|
|
329
277
|
|
|
330
|
-
export {
|
|
331
|
-
ROOT,
|
|
332
|
-
TOOLS_DIR,
|
|
333
|
-
NAME_RE,
|
|
334
|
-
SIZE_CAP,
|
|
335
|
-
STDLIB,
|
|
336
|
-
PROJECT_PACKAGES,
|
|
337
|
-
lint,
|
|
338
|
-
main,
|
|
339
|
-
};
|
|
278
|
+
export { ROOT, TOOLS_DIR, NAME_RE, lint, main };
|
|
@@ -41,6 +41,8 @@ import * as fs from 'node:fs';
|
|
|
41
41
|
import * as path from 'node:path';
|
|
42
42
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
43
43
|
|
|
44
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
45
|
+
|
|
44
46
|
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
45
47
|
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
46
48
|
|
|
@@ -204,9 +206,14 @@ function scanMarkdownTranscript(filePath: string, repoRoot: string): Finding[] {
|
|
|
204
206
|
|
|
205
207
|
function scanRepo(repoRoot: string): Finding[] {
|
|
206
208
|
const findings: Finding[] = [];
|
|
209
|
+
// Counted before the `*spawn*.json` / spawn-context filters: those decide
|
|
210
|
+
// what is JUDGED, and both are legitimately zero on a clean tree. Only the
|
|
211
|
+
// walk itself distinguishes "no spawn payloads" from "no files read".
|
|
212
|
+
let scanned = 0;
|
|
207
213
|
|
|
208
214
|
const fixturesDir = path.join(repoRoot, 'tests', 'fixtures');
|
|
209
215
|
for (const f of walkFiles(fixturesDir)) {
|
|
216
|
+
scanned += 1;
|
|
210
217
|
if (f.endsWith('.json') && /spawn/i.test(path.basename(f))) {
|
|
211
218
|
findings.push(...scanJsonFixture(f, repoRoot));
|
|
212
219
|
}
|
|
@@ -216,11 +223,18 @@ function scanRepo(repoRoot: string): Finding[] {
|
|
|
216
223
|
if (fs.existsSync(transcriptsDir)) {
|
|
217
224
|
for (const entry of fs.readdirSync(transcriptsDir, { withFileTypes: true })) {
|
|
218
225
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
226
|
+
scanned += 1;
|
|
219
227
|
findings.push(...scanMarkdownTranscript(path.join(transcriptsDir, entry.name), repoRoot));
|
|
220
228
|
}
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
231
|
|
|
232
|
+
assertScanned({
|
|
233
|
+
gate: 'lint_spawn_payload',
|
|
234
|
+
scanned,
|
|
235
|
+
units: 'fixture / transcript file(s)',
|
|
236
|
+
roots: ['tests/fixtures', 'tests/reasoning-layer-eval/golden-transcripts'],
|
|
237
|
+
});
|
|
224
238
|
return findings;
|
|
225
239
|
}
|
|
226
240
|
|
|
@@ -234,7 +248,19 @@ function parseArgs(argv: readonly string[]): { strict: boolean; quiet: boolean }
|
|
|
234
248
|
function main(argv?: readonly string[], repoRootOverride?: string): number {
|
|
235
249
|
const args = parseArgs(argv ?? process.argv.slice(2));
|
|
236
250
|
const root = repoRootOverride ?? REPO_ROOT;
|
|
237
|
-
|
|
251
|
+
let findings: Finding[];
|
|
252
|
+
try {
|
|
253
|
+
findings = scanRepo(root);
|
|
254
|
+
} catch (exc) {
|
|
255
|
+
if (exc instanceof DeadScopeError) {
|
|
256
|
+
// 2 is this gate's only non-zero code. Returning it without
|
|
257
|
+
// `--strict` is deliberate: warn-only governs FINDINGS, and a dead
|
|
258
|
+
// scope means there were none to warn about.
|
|
259
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
throw exc;
|
|
263
|
+
}
|
|
238
264
|
|
|
239
265
|
if (!args.quiet) {
|
|
240
266
|
for (const f of findings) {
|
|
@@ -29,6 +29,8 @@ import fs from 'node:fs';
|
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
|
+
|
|
32
34
|
const _HERE = fileURLToPath(import.meta.url);
|
|
33
35
|
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
34
36
|
|
|
@@ -113,6 +115,23 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
113
115
|
return 2;
|
|
114
116
|
}
|
|
115
117
|
}
|
|
118
|
+
// SCAN_FILES is a hand-maintained watch list and `scanFile` swallows a read
|
|
119
|
+
// error, so a renamed or moved index module drops out of scope silently and
|
|
120
|
+
// the gate reports "clean" over the files it can no longer read. Exit 3 is
|
|
121
|
+
// the "this gate could not run" code; 1 stays "violations found".
|
|
122
|
+
try {
|
|
123
|
+
assertWatchlistResolves({
|
|
124
|
+
gate: 'lint_store_boundary',
|
|
125
|
+
candidates: SCAN_FILES,
|
|
126
|
+
repoRoot: root,
|
|
127
|
+
});
|
|
128
|
+
} catch (exc) {
|
|
129
|
+
if (exc instanceof DeadScopeError) {
|
|
130
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
131
|
+
return 3;
|
|
132
|
+
}
|
|
133
|
+
throw exc;
|
|
134
|
+
}
|
|
116
135
|
let violations: Violation[];
|
|
117
136
|
try {
|
|
118
137
|
violations = run(root);
|
|
@@ -24,6 +24,7 @@ import * as fs from 'node:fs';
|
|
|
24
24
|
import * as path from 'node:path';
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
26
|
|
|
27
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
27
28
|
import { parse_frontmatter } from './validate_frontmatter.js';
|
|
28
29
|
|
|
29
30
|
const QUIET = process.argv.includes('--quiet');
|
|
@@ -47,7 +48,28 @@ function main(dir: string = SUBAGENT_DIR): number {
|
|
|
47
48
|
const findings: string[] = [];
|
|
48
49
|
const namesSeen = new Map<string, string>();
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
const files = _listSubagents(dir);
|
|
52
|
+
// The unit is every `.md` walked, not `namesSeen` — that map only grows on
|
|
53
|
+
// units that already passed the stem-match, so it reads 0 both for a moved
|
|
54
|
+
// root and for a corpus where every unit is broken.
|
|
55
|
+
try {
|
|
56
|
+
assertScanned({
|
|
57
|
+
gate: 'lint_subagent_determinism',
|
|
58
|
+
scanned: files.length,
|
|
59
|
+
units: 'subagent file(s)',
|
|
60
|
+
roots: [dir],
|
|
61
|
+
});
|
|
62
|
+
} catch (exc) {
|
|
63
|
+
if (exc instanceof DeadScopeError) {
|
|
64
|
+
// 1 is this gate's only non-zero code; here it means "could not
|
|
65
|
+
// run", not "found a determinism violation".
|
|
66
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
throw exc;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const file of files) {
|
|
51
73
|
const rel = path.relative(REPO, file);
|
|
52
74
|
const stem = path.basename(file, '.md');
|
|
53
75
|
const text = fs.readFileSync(file, 'utf-8');
|
|
@@ -12,6 +12,7 @@ import * as path from 'node:path';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
|
|
14
14
|
import { lint_matrix } from './_lib/tool_adapter_registry.js';
|
|
15
|
+
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
15
16
|
|
|
16
17
|
const _HERE = fileURLToPath(import.meta.url);
|
|
17
18
|
const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
|
|
@@ -25,6 +26,23 @@ const isMain = process.argv[1] !== undefined && path.resolve(process.argv[1]) ==
|
|
|
25
26
|
if (isMain) {
|
|
26
27
|
const idx = process.argv.indexOf('--readme');
|
|
27
28
|
const readmePath = idx !== -1 ? (process.argv[idx + 1] ?? '') : path.join(REPO_ROOT, 'README.md');
|
|
29
|
+
// The whole README leg of this gate is one named file. A moved README turns
|
|
30
|
+
// `run()` into an ENOENT stack trace, which reads as a crash rather than as
|
|
31
|
+
// a gate whose scope died — name the scope first. Exit 1 is the gate's only
|
|
32
|
+
// failure code; here it means "could not run", not "found drift".
|
|
33
|
+
try {
|
|
34
|
+
assertWatchlistResolves({
|
|
35
|
+
gate: 'lint_supported_tools_matrix',
|
|
36
|
+
candidates: [path.relative(REPO_ROOT, path.resolve(readmePath))],
|
|
37
|
+
repoRoot: REPO_ROOT,
|
|
38
|
+
});
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (e instanceof DeadScopeError) {
|
|
41
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
28
46
|
const errors = run(readmePath);
|
|
29
47
|
if (errors.length > 0) {
|
|
30
48
|
for (const e of errors) process.stderr.write(`❌ ${e}\n`);
|