@event4u/agent-config 9.14.0 → 9.15.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 +110 -112
- package/README.md +8 -0
- package/dist/agent-src/commands/agents/audit.md +7 -7
- package/dist/agent-src/commands/agents/optimize.md +1 -1
- package/dist/agent-src/commands/analyze/reference-repo.md +1 -1
- package/dist/agent-src/commands/chat-history.md +1 -0
- package/dist/agent-src/commands/context.md +1 -0
- package/dist/agent-src/commands/council.md +1 -1
- package/dist/agent-src/commands/ghostwriter.md +1 -1
- package/dist/agent-src/commands/memory.md +2 -1
- package/dist/agent-src/commands/module.md +1 -0
- package/dist/agent-src/commands/optimize/agents-dir.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +5 -2
- package/dist/agent-src/commands/optimize/skills.md +5 -5
- package/dist/agent-src/commands/override.md +1 -0
- package/dist/agent-src/commands/prediction-pool.md +4 -4
- package/dist/agent-src/commands/research/deep.md +1 -1
- package/dist/agent-src/commands/roadmap.md +2 -1
- package/dist/agent-src/commands/rule-compliance-audit.md +1 -1
- package/dist/agent-src/commands/tdd.md +1 -0
- package/dist/agent-src/commands/tests.md +1 -0
- package/dist/agent-src/commands/upstream-contribute.md +5 -5
- package/dist/agent-src/commands/video/from-script.md +1 -1
- package/dist/agent-src/commands/video/from-song.md +1 -1
- package/dist/agent-src/commands/video/scene.md +1 -1
- package/dist/agent-src/commands/video/stitch.md +1 -1
- package/dist/agent-src/commands/video/storyboard.md +1 -1
- package/dist/agent-src/commands/worktree/create.md +2 -1
- package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
- package/dist/agent-src/contexts/contracts/artifact-engagement-flow.md +5 -5
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +3 -3
- package/dist/agent-src/contexts/contracts/emergency-triage-block.md +2 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +4 -3
- package/dist/agent-src/contexts/execution/subagent-routing.md +19 -0
- package/dist/agent-src/contexts/override-system.md +1 -1
- package/dist/agent-src/ghostwriter/fictional-fixture-v1.md +1 -1
- package/dist/agent-src/rules/augment-edit-discipline.md +1 -1
- package/dist/agent-src/rules/autonomous-execution.md +2 -1
- package/dist/agent-src/rules/brand-consistency.md +1 -0
- package/dist/agent-src/rules/brand-source-of-truth.md +1 -0
- package/dist/agent-src/rules/code-comment-discipline.md +2 -0
- package/dist/agent-src/rules/communication-through-line.md +1 -0
- package/dist/agent-src/rules/content-quoting-floor.md +1 -0
- package/dist/agent-src/rules/cross-source-consistency.md +0 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +1 -0
- package/dist/agent-src/rules/external-reference-deep-dive.md +1 -0
- package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
- package/dist/agent-src/rules/finance-safety-floor.md +0 -1
- package/dist/agent-src/rules/icon-consistency.md +1 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
- package/dist/agent-src/rules/improve-before-implement.md +1 -1
- package/dist/agent-src/rules/invite-challenge.md +1 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
- package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/dist/agent-src/rules/media-governance-routing.md +4 -3
- package/dist/agent-src/rules/media-sync-ground-truth.md +1 -0
- package/dist/agent-src/rules/no-attribution-footers.md +1 -0
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +1 -0
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -0
- package/dist/agent-src/rules/onboarding-gate.md +1 -0
- package/dist/agent-src/rules/output-discipline.md +1 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +1 -0
- package/dist/agent-src/rules/preservation-guard.md +1 -0
- package/dist/agent-src/rules/question-not-instruction.md +1 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +1 -0
- package/dist/agent-src/rules/role-mode-adherence.md +1 -0
- package/dist/agent-src/rules/runtime-safety.md +1 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +1 -0
- package/dist/agent-src/rules/session-canary.md +9 -2
- package/dist/agent-src/rules/source-confidentiality.md +1 -0
- package/dist/agent-src/rules/source-of-truth.md +1 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/token-budget-discipline.md +1 -0
- package/dist/agent-src/rules/token-efficiency.md +1 -0
- package/dist/agent-src/rules/token-optimizer-maintenance.md +5 -5
- package/dist/agent-src/rules/tool-safety.md +1 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +1 -0
- package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
- package/dist/agent-src/skills/adr-create/SKILL.md +1 -1
- package/dist/agent-src/skills/check-refs/SKILL.md +1 -1
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +1 -1
- package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
- package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +1 -1
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/github-ci/SKILL.md +1 -0
- package/dist/agent-src/skills/guideline-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/lint-skills/SKILL.md +1 -1
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +8 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -4
- package/dist/agent-src/skills/token-optimizer/SKILL.md +4 -4
- package/dist/agent-src/skills/upstream-contribute/SKILL.md +6 -6
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +25 -0
- package/dist/agent-src/templates/AGENTS.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/command.md +1 -1
- package/dist/agent-src/templates/copilot-instructions.md +3 -3
- package/dist/agent-src/templates/rule.md +2 -2
- package/dist/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/dist/agent-src/templates/skill-archive-note.md +5 -5
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/agent-config.js +66 -19
- package/dist/cli/agent-config.js.map +1 -1
- package/dist/cli/main.js +9 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/python/workspace_drive.js +2 -0
- package/dist/cli/python/workspace_drive.js.map +1 -1
- package/dist/cli/python/workspace_drive_health.js +2 -0
- package/dist/cli/python/workspace_drive_health.js.map +1 -1
- package/dist/cli/python/workspace_hosts.js +2 -0
- package/dist/cli/python/workspace_hosts.js.map +1 -1
- package/dist/cli/python/workspace_sessions.js +2 -0
- package/dist/cli/python/workspace_sessions.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-A3HJ6NTI.js → chunk-63WSP6YZ.js} +2 -1
- package/dist/cli-delegate/{chunk-EJYIGZ3T.js → chunk-BKO2AZ7Z.js} +1 -1
- package/dist/cli-delegate/{chunk-GCVFLBP2.js → chunk-H574KOUH.js} +338 -494
- package/dist/cli-delegate/chunk-NXFWFAYV.js +339 -0
- package/dist/cli-delegate/{chunk-TEGJU2JX.js → chunk-ZXXJLL5K.js} +2 -2
- package/dist/cli-delegate/cmd_conformance.js +29 -23
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +127 -95
- 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 +729 -430
- package/dist/install/install.mjs +52 -11
- package/dist/install/rule_scope.js +18 -2
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +518 -411
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_settings_hooks.js +30 -5
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
- package/dist/scripts/_lib/scoped_projection.js +223 -0
- package/dist/scripts/_lib/scoped_projection.js.map +1 -0
- package/dist/scripts/_lib/surface_tiers.js +125 -0
- package/dist/scripts/_lib/surface_tiers.js.map +1 -0
- package/dist/scripts/_lib/token_count.js +166 -0
- package/dist/scripts/_lib/token_count.js.map +1 -0
- package/dist/scripts/_lib/trigger_eval_floors.js +8 -0
- package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -1
- package/dist/scripts/condense.js +139 -9
- package/dist/scripts/condense.js.map +1 -1
- package/dist/scripts/project_thin_rules.js +450 -0
- package/dist/scripts/project_thin_rules.js.map +1 -0
- package/dist/server/routes/workspace.js +2 -0
- package/dist/server/routes/workspace.js.map +1 -1
- package/dist/server/schemas/settings.js +9 -3
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/index-DJY_vQVn.js +40 -0
- package/dist/ui/assets/index-DJY_vQVn.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/package.json +11 -11
- package/src/agent-src/templates/AGENTS.md +1 -1
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +1 -1
- package/src/config/agent-settings.template.yml +59 -11
- package/src/config/ci-local-parity.yml +8 -0
- package/src/config/discovery/unassigned-artefacts.yml +23 -25
- package/src/config/evaluator-budgets.json +35 -5
- package/src/config/gate-coverage.yml +147 -0
- package/src/config/gate-violation-baselines.json +25 -0
- package/src/install/rule_scope.ts +27 -2
- package/src/scripts/_cli/cmd_conformance.ts +13 -24
- package/src/scripts/_dispatch.bash +10 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +30 -4
- package/src/scripts/_lib/gate_baseline.ts +213 -0
- package/src/scripts/_lib/scoped_projection.ts +50 -0
- package/src/scripts/_lib/tier_budget_routing.ts +234 -0
- package/src/scripts/_lib/tool_adapter_registry.ts +8 -1
- package/src/scripts/_lib/trigger_eval_floors.ts +9 -0
- package/src/scripts/_lib/trigger_routers.ts +17 -5
- package/src/scripts/adr/regenerate_index.ts +4 -3
- package/src/scripts/ai-video/lib/parse-blueprint.sh +1 -1
- package/src/scripts/ai_council/compile_corpus.ts +1 -1
- package/src/scripts/audit_cloud_compatibility.ts +37 -2
- package/src/scripts/audit_initial_context.ts +53 -19
- package/src/scripts/audit_likelihood.ts +31 -2
- package/src/scripts/audit_user_type_axis.ts +58 -11
- package/src/scripts/bench_hook_latency.ts +172 -41
- package/src/scripts/build_mcp_catalog.ts +12 -1
- package/src/scripts/check_always_budget.ts +122 -11
- package/src/scripts/check_augment_description_cap.ts +24 -3
- package/src/scripts/check_ci_strict_superset.ts +150 -0
- package/src/scripts/check_cluster_patterns.ts +100 -0
- package/src/scripts/check_consumer_scope_flip.ts +26 -4
- package/src/scripts/check_evaluator_budgets.ts +198 -19
- package/src/scripts/check_gate_coverage.ts +337 -1
- package/src/scripts/check_generator_output_coverage.ts +2 -1
- package/src/scripts/check_iron_law_prominence.ts +1 -1
- package/src/scripts/check_kernel_rule_bundle.ts +1 -1
- package/src/scripts/check_no_new_legacy_path.ts +167 -3
- package/src/scripts/check_pack_size.ts +18 -4
- package/src/scripts/check_portability.ts +37 -7
- package/src/scripts/check_references.ts +19 -3
- package/src/scripts/check_release_pr_shape.ts +13 -0
- package/src/scripts/check_token_regression.ts +9 -2
- package/src/scripts/condense.ts +155 -12
- package/src/scripts/cost/budget.mjs +63 -2
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/generate_capability_matrix.ts +8 -0
- package/src/scripts/generate_router_coverage_from_matrix.ts +155 -0
- package/src/scripts/hook_manifest.yaml +17 -3
- package/src/scripts/hooks/block_config_weakening.ts +337 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/probe_throttle.ts +75 -0
- package/src/scripts/install-hooks.sh +24 -2
- package/src/scripts/install.ts +2 -2
- package/src/scripts/lint_agents_md.ts +9 -3
- package/src/scripts/lint_archived_skills.ts +1 -1
- package/src/scripts/lint_artefact_frontmatter.ts +185 -37
- package/src/scripts/lint_command_routing.ts +21 -52
- package/src/scripts/lint_command_verbs.ts +47 -28
- package/src/scripts/lint_context_spine_usage.ts +38 -12
- package/src/scripts/lint_framework_leakage.ts +25 -6
- package/src/scripts/lint_handoffs.ts +49 -2
- package/src/scripts/lint_media_policy_linkage.ts +62 -30
- package/src/scripts/lint_namespace.ts +92 -7
- package/src/scripts/lint_new_skill_gate.ts +39 -31
- package/src/scripts/lint_originality_shingles.ts +7 -1
- package/src/scripts/lint_override_kernel_guard.ts +21 -3
- package/src/scripts/lint_pack_boundaries.ts +461 -91
- package/src/scripts/lint_pack_dependencies.ts +24 -57
- package/src/scripts/lint_persona_governance.ts +1 -1
- package/src/scripts/lint_role_experiences.ts +34 -5
- package/src/scripts/lint_trigger_precision.ts +142 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +12 -201
- package/src/scripts/mcp_server/tools.ts +1 -1
- package/src/scripts/prepack-check.mjs +40 -1
- package/src/scripts/prepack_router_targets.d.mts +7 -0
- package/src/scripts/prepack_router_targets.mjs +93 -0
- package/src/scripts/print_required_checks.ts +72 -18
- package/src/scripts/profile_staleness_hook.ts +12 -0
- package/src/scripts/record_evaluator_measurements.mjs +105 -0
- package/src/scripts/recruit_preflight.sh +1 -1
- package/src/scripts/release.ts +105 -1
- package/src/scripts/router_target_paths.d.mts +4 -0
- package/src/scripts/router_target_paths.mjs +69 -0
- package/src/scripts/router_telemetry.ts +52 -1
- package/src/scripts/routing_doctor.ts +485 -0
- package/src/scripts/rule_trigger_eval.ts +403 -0
- package/src/scripts/schemas/rule.schema.json +5 -1
- package/src/scripts/session_canary_hook.ts +96 -5
- package/src/scripts/skill_linter.ts +9 -28
- package/src/scripts/smoke/skills.sh +1 -1
- package/src/scripts/sweep_dead_scan_roots.ts +356 -10
- package/src/scripts/trigger_coverage.ts +13 -2
- package/src/scripts/update_counts.ts +1 -1
- package/src/scripts/validate_telegraph_carveouts.ts +1 -1
- package/src/scripts/wrapper_freshness_hook.ts +14 -0
- package/src/server/schemas/settings.ts +19 -5
- package/dist/cli-delegate/chunk-TJIAM5FW.js +0 -133
- package/dist/ui/assets/index-BpSQt5vm.js +0 -40
- package/dist/ui/assets/index-BpSQt5vm.js.map +0 -1
|
@@ -23,13 +23,25 @@
|
|
|
23
23
|
* the manifest is empty or unreadable, and it REPORTS `pending` gates (listed but
|
|
24
24
|
* not yet emitting a count) instead of skipping them silently.
|
|
25
25
|
*
|
|
26
|
+
* MUTATION CANARY (`--canary`, road-to-gates-that-can-fail Phase 7). Coverage
|
|
27
|
+
* proves a gate READ something; it cannot prove the gate can still FAIL. The
|
|
28
|
+
* canary plants one declared violation per gate, runs the gate, and records
|
|
29
|
+
* red/green — a gate that stays green over a real planted defect is dead by
|
|
30
|
+
* definition. It runs the same contract as the review-side canary in
|
|
31
|
+
* `docs/contracts/adversarial-review-protocol.md` § 6 (rotating class, sealed
|
|
32
|
+
* record, NEVER SHIPS) and is deliberately kept OFF the default CI path: it
|
|
33
|
+
* mutates the tree, so it is an operator-invoked biannual experiment, not a
|
|
34
|
+
* per-PR gate. Every plant is reverted in a `finally`.
|
|
35
|
+
*
|
|
26
36
|
* CLI:
|
|
27
37
|
* ./scripts-run src/scripts/check_gate_coverage # enforce
|
|
28
38
|
* ./scripts-run src/scripts/check_gate_coverage --list # show the manifest
|
|
29
39
|
* ./scripts-run src/scripts/check_gate_coverage --format json
|
|
40
|
+
* ./scripts-run src/scripts/check_gate_coverage --canary [--ledger <file>]
|
|
30
41
|
*
|
|
31
42
|
* Exit codes: 0 all enforced gates cleared their floor · 1 a gate is blind,
|
|
32
|
-
* collapsed, or silent
|
|
43
|
+
* collapsed, or silent (in `--canary`: a gate stayed green, or the ledger
|
|
44
|
+
* disagrees with the census) · 2 the manifest itself is missing/empty/malformed.
|
|
33
45
|
*/
|
|
34
46
|
import * as fs from 'node:fs';
|
|
35
47
|
import * as path from 'node:path';
|
|
@@ -42,16 +54,36 @@ import { runCountedProbe } from './_lib/counted_probe.js';
|
|
|
42
54
|
const _HERE = fileURLToPath(import.meta.url);
|
|
43
55
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
44
56
|
const MANIFEST = path.join(REPO_ROOT, 'src/config/gate-coverage.yml');
|
|
57
|
+
const CENSUS = path.join(REPO_ROOT, 'agents/evidence/reports/gate-scope-census.md');
|
|
45
58
|
|
|
46
59
|
/** The one line a gate must emit. Deliberately narrow: no format guessing. */
|
|
47
60
|
const SCANNED_RE = /^\s*scanned:\s*(\d+)\s*$/m;
|
|
48
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A declared, revertible violation for one gate (Phase 7).
|
|
64
|
+
*
|
|
65
|
+
* Only ONE operation shape exists on purpose: create a file that does not exist,
|
|
66
|
+
* delete it afterwards. An in-place edit would need byte-exact restoration of a
|
|
67
|
+
* tracked file, and a canary that can corrupt the tree is worse than no canary —
|
|
68
|
+
* the contract's "never ships" rule binds this implementation too.
|
|
69
|
+
*/
|
|
70
|
+
export interface CanarySpec {
|
|
71
|
+
/** Rotating-class label, per adversarial-review-protocol § 6. */
|
|
72
|
+
class: string;
|
|
73
|
+
/** Repo-relative path to create. MUST NOT already exist. */
|
|
74
|
+
path: string;
|
|
75
|
+
/** File body planted at that path. */
|
|
76
|
+
content: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
export interface GateSpec {
|
|
50
80
|
id: string;
|
|
51
81
|
argv: string[];
|
|
52
82
|
min_scanned: number;
|
|
53
83
|
corpus: string;
|
|
54
84
|
status: 'enforced' | 'pending';
|
|
85
|
+
/** Absent = this gate has no canary recipe; reported, never hidden. */
|
|
86
|
+
canary?: CanarySpec;
|
|
55
87
|
/**
|
|
56
88
|
* Exit code meaning "my prerequisite is absent, so I inspected nothing" — e.g.
|
|
57
89
|
* check_site_links exits 2 when the site is unbuilt or stale. Without this the
|
|
@@ -117,10 +149,27 @@ export function load_manifest(file = MANIFEST): GateSpec[] {
|
|
|
117
149
|
? {}
|
|
118
150
|
: { unavailable_exit: _require_int(e['unavailable_exit'], id, i) }),
|
|
119
151
|
...(e['note'] === undefined ? {} : { note: String(e['note']) }),
|
|
152
|
+
...(e['canary'] === undefined ? {} : { canary: _require_canary(e['canary'], id, i) }),
|
|
120
153
|
};
|
|
121
154
|
});
|
|
122
155
|
}
|
|
123
156
|
|
|
157
|
+
function _require_canary(v: unknown, id: string, i: number): CanarySpec {
|
|
158
|
+
const c = v as Record<string, unknown>;
|
|
159
|
+
const cls = String(c?.['class'] ?? '');
|
|
160
|
+
const p = String(c?.['path'] ?? '');
|
|
161
|
+
const content = String(c?.['content'] ?? '');
|
|
162
|
+
// A half-declared recipe would plant nothing and read as a passing canary —
|
|
163
|
+
// the exact false green this whole file exists to prevent.
|
|
164
|
+
if (cls === '' || p === '' || content === '') {
|
|
165
|
+
throw new Error(`gates[${String(i)}] (${id}): canary needs class, path and content`);
|
|
166
|
+
}
|
|
167
|
+
if (path.isAbsolute(p) || p.split('/').includes('..')) {
|
|
168
|
+
throw new Error(`gates[${String(i)}] (${id}): canary path must be repo-relative and contain no '..'`);
|
|
169
|
+
}
|
|
170
|
+
return { class: cls, path: p, content };
|
|
171
|
+
}
|
|
172
|
+
|
|
124
173
|
/** Extract the contract line. Returns null when the gate emitted no count. */
|
|
125
174
|
export function parse_scanned(output: string): number | null {
|
|
126
175
|
const m = SCANNED_RE.exec(output);
|
|
@@ -215,6 +264,10 @@ export function main(argv: readonly string[]): number {
|
|
|
215
264
|
return 2;
|
|
216
265
|
}
|
|
217
266
|
|
|
267
|
+
if (argv.includes('--canary')) {
|
|
268
|
+
return run_canary_mode(specs, argv);
|
|
269
|
+
}
|
|
270
|
+
|
|
218
271
|
if (listOnly) {
|
|
219
272
|
for (const s of specs) {
|
|
220
273
|
process.stdout.write(
|
|
@@ -283,6 +336,289 @@ function probe_pending(s: GateSpec): { scanned: number | null; crashed: boolean
|
|
|
283
336
|
return run_gate(s);
|
|
284
337
|
}
|
|
285
338
|
|
|
339
|
+
// ── Mutation canary (Phase 7) ──────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
export type CanaryVerdict = 'red' | 'green' | 'no_recipe' | 'crashed';
|
|
342
|
+
|
|
343
|
+
export interface CanaryResult {
|
|
344
|
+
id: string;
|
|
345
|
+
verdict: CanaryVerdict;
|
|
346
|
+
class: string | null;
|
|
347
|
+
exit_code: number | null;
|
|
348
|
+
message: string;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Plant one declared violation, run the gate, revert.
|
|
353
|
+
*
|
|
354
|
+
* The revert is in a `finally` and deletes exactly the file this function
|
|
355
|
+
* created — never a pre-existing one, which is why an occupied path aborts
|
|
356
|
+
* instead of overwriting.
|
|
357
|
+
*/
|
|
358
|
+
export function run_canary(spec: GateSpec, repoRoot = REPO_ROOT): CanaryResult {
|
|
359
|
+
const c = spec.canary;
|
|
360
|
+
if (c === undefined) {
|
|
361
|
+
return {
|
|
362
|
+
id: spec.id,
|
|
363
|
+
verdict: 'no_recipe',
|
|
364
|
+
class: null,
|
|
365
|
+
exit_code: null,
|
|
366
|
+
message: 'no canary recipe declared — this gate is UNPROVEN, not proven working',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const abs = path.join(repoRoot, c.path);
|
|
370
|
+
if (fs.existsSync(abs)) {
|
|
371
|
+
return {
|
|
372
|
+
id: spec.id,
|
|
373
|
+
verdict: 'crashed',
|
|
374
|
+
class: c.class,
|
|
375
|
+
exit_code: null,
|
|
376
|
+
message: `canary path ${c.path} already exists — refusing to overwrite a real file`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
let planted = false;
|
|
380
|
+
// Directories the plant had to create. An empty `src/skills/<x>/` left behind
|
|
381
|
+
// is invisible to `git status` but is still residue, and a skill directory
|
|
382
|
+
// with no SKILL.md is itself a defect the next gate run would report.
|
|
383
|
+
const madeDirs: string[] = [];
|
|
384
|
+
for (let d = path.dirname(abs); !fs.existsSync(d) && d.startsWith(repoRoot); d = path.dirname(d)) {
|
|
385
|
+
madeDirs.push(d);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
389
|
+
fs.writeFileSync(abs, c.content, 'utf8');
|
|
390
|
+
planted = true;
|
|
391
|
+
const runner = path.join(repoRoot, 'scripts-run');
|
|
392
|
+
const r = runCountedProbe(runner, [`src/scripts/${spec.id}`, ...spec.argv], { cwd: repoRoot });
|
|
393
|
+
if (r.failure !== null && r.status === null) {
|
|
394
|
+
return {
|
|
395
|
+
id: spec.id,
|
|
396
|
+
verdict: 'crashed',
|
|
397
|
+
class: c.class,
|
|
398
|
+
exit_code: null,
|
|
399
|
+
message: `gate could not be executed under the plant: ${r.failure}`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const code = r.status ?? 0;
|
|
403
|
+
return code !== 0
|
|
404
|
+
? {
|
|
405
|
+
id: spec.id,
|
|
406
|
+
verdict: 'red',
|
|
407
|
+
class: c.class,
|
|
408
|
+
exit_code: code,
|
|
409
|
+
message: `caught the planted ${c.class} defect (exit ${String(code)})`,
|
|
410
|
+
}
|
|
411
|
+
: {
|
|
412
|
+
id: spec.id,
|
|
413
|
+
verdict: 'green',
|
|
414
|
+
class: c.class,
|
|
415
|
+
exit_code: 0,
|
|
416
|
+
message:
|
|
417
|
+
`stayed GREEN over a planted ${c.class} defect at ${c.path} — ` +
|
|
418
|
+
'the gate is dead by definition. This row is a defect ticket',
|
|
419
|
+
};
|
|
420
|
+
} finally {
|
|
421
|
+
// NEVER SHIPS (adversarial-review-protocol § 6): the plant is removed even
|
|
422
|
+
// when the gate crashes or the process is interrupted mid-run.
|
|
423
|
+
if (planted) {
|
|
424
|
+
try {
|
|
425
|
+
fs.rmSync(abs, { force: true });
|
|
426
|
+
// Innermost first — rmdir only succeeds while the directory is empty,
|
|
427
|
+
// so a dir that gained other content is left alone.
|
|
428
|
+
for (const d of madeDirs) {
|
|
429
|
+
try {
|
|
430
|
+
fs.rmdirSync(d);
|
|
431
|
+
} catch {
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
process.stderr.write(`❌ canary could not remove its plant at ${c.path} — REMOVE IT BY HAND\n`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export interface Disagreement {
|
|
443
|
+
gate: string;
|
|
444
|
+
kind: 'dead_gate' | 'census_stale';
|
|
445
|
+
detail: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Read the scan-scope census into `gate -> units`.
|
|
450
|
+
*
|
|
451
|
+
* Column-position-agnostic: it locates the gate column and the units column by
|
|
452
|
+
* header text, so a census regenerated with a different column set still
|
|
453
|
+
* cross-checks. `null` = the census recorded no measurable count.
|
|
454
|
+
*/
|
|
455
|
+
export function parse_census(md: string): Map<string, number | null> {
|
|
456
|
+
const out = new Map<string, number | null>();
|
|
457
|
+
const rows = md.split('\n').filter((l) => l.trim().startsWith('|'));
|
|
458
|
+
const cells = (l: string): string[] =>
|
|
459
|
+
l.split('|').slice(1, -1).map((c) => c.trim());
|
|
460
|
+
let gateCol = -1;
|
|
461
|
+
let unitCol = -1;
|
|
462
|
+
for (const line of rows) {
|
|
463
|
+
const cs = cells(line);
|
|
464
|
+
const head = cs.map((c) => c.toLowerCase());
|
|
465
|
+
const g = head.findIndex((c) => c === 'gate');
|
|
466
|
+
const u = head.findIndex((c) => c.startsWith('unit'));
|
|
467
|
+
if (g >= 0 && u >= 0) {
|
|
468
|
+
gateCol = g;
|
|
469
|
+
unitCol = u;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (gateCol < 0 || cs.length <= Math.max(gateCol, unitCol)) continue;
|
|
473
|
+
const idm = /`([A-Za-z0-9_.-]+)`/.exec(cs[gateCol] as string);
|
|
474
|
+
if (idm === null) continue;
|
|
475
|
+
const raw = cs[unitCol] as string;
|
|
476
|
+
const num = /(\d[\d,]*)/.exec(raw.replace(/\*/g, ''));
|
|
477
|
+
out.set(idm[1] as string, num === null ? null : Number((num[1] as string).replace(/,/g, '')));
|
|
478
|
+
}
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Make the two artefacts disagree loudly (Phase 7 step 2).
|
|
484
|
+
*
|
|
485
|
+
* The census says what a gate READS; the canary says whether anything it reads
|
|
486
|
+
* can make it FAIL. Each alone is satisfiable by a broken gate; together they
|
|
487
|
+
* are not.
|
|
488
|
+
*/
|
|
489
|
+
export function cross_check(
|
|
490
|
+
results: readonly CanaryResult[],
|
|
491
|
+
census: ReadonlyMap<string, number | null>,
|
|
492
|
+
): Disagreement[] {
|
|
493
|
+
const out: Disagreement[] = [];
|
|
494
|
+
for (const r of results) {
|
|
495
|
+
const units = census.get(r.id);
|
|
496
|
+
if (units === undefined) continue; // not censused — nothing to disagree with
|
|
497
|
+
if (r.verdict === 'green' && units !== null && units > 0) {
|
|
498
|
+
out.push({
|
|
499
|
+
gate: r.id,
|
|
500
|
+
kind: 'dead_gate',
|
|
501
|
+
detail:
|
|
502
|
+
`census records ${String(units)} unit(s) read, but the canary could not make it fail — ` +
|
|
503
|
+
'a gate that reads a live corpus and cannot go red is dead',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (r.verdict === 'red' && (units === null || units === 0)) {
|
|
507
|
+
out.push({
|
|
508
|
+
gate: r.id,
|
|
509
|
+
kind: 'census_stale',
|
|
510
|
+
detail:
|
|
511
|
+
'the canary made it fail, so it reads a live corpus, but the census records no units — ' +
|
|
512
|
+
're-run the census; its row is stale',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const CANARY_ICON: Record<CanaryVerdict, string> = {
|
|
520
|
+
red: '✅',
|
|
521
|
+
green: '❌',
|
|
522
|
+
no_recipe: '⚠️',
|
|
523
|
+
crashed: '❌',
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
export function render_ledger(
|
|
527
|
+
results: readonly CanaryResult[],
|
|
528
|
+
disagreements: readonly Disagreement[],
|
|
529
|
+
totalGateScripts: number,
|
|
530
|
+
cycleId: string,
|
|
531
|
+
): string {
|
|
532
|
+
const recipes = results.filter((r) => r.verdict !== 'no_recipe');
|
|
533
|
+
const red = results.filter((r) => r.verdict === 'red');
|
|
534
|
+
const green = results.filter((r) => r.verdict === 'green');
|
|
535
|
+
const lines = [
|
|
536
|
+
`# Gate-surface canary ledger — ${cycleId}`,
|
|
537
|
+
'',
|
|
538
|
+
'Produced by `./scripts-run src/scripts/check_gate_coverage --canary`. Governed by',
|
|
539
|
+
'[`adversarial-review-protocol`](../../../../docs/contracts/adversarial-review-protocol.md)',
|
|
540
|
+
'§ 6 — biannual cadence, rotating class, sealed record, never ships.',
|
|
541
|
+
'',
|
|
542
|
+
'## Coverage (stated, not implied)',
|
|
543
|
+
'',
|
|
544
|
+
`- Gate scripts in \`src/scripts/\`: **${String(totalGateScripts)}**`,
|
|
545
|
+
`- Listed in \`src/config/gate-coverage.yml\`: **${String(results.length)}**`,
|
|
546
|
+
`- Carrying a canary recipe: **${String(recipes.length)}**`,
|
|
547
|
+
`- RED (caught the plant): **${String(red.length)}** · GREEN (dead): **${String(green.length)}**`,
|
|
548
|
+
'',
|
|
549
|
+
'Every gate outside the recipe count is UNPROVEN by this experiment. That is a',
|
|
550
|
+
'gap, not a pass.',
|
|
551
|
+
'',
|
|
552
|
+
'## Per-gate ledger',
|
|
553
|
+
'',
|
|
554
|
+
'| Gate | Class | Verdict | Exit | Detail |',
|
|
555
|
+
'|---|---|---|---:|---|',
|
|
556
|
+
];
|
|
557
|
+
for (const r of results) {
|
|
558
|
+
lines.push(
|
|
559
|
+
`| \`${r.id}\` | ${r.class ?? '—'} | ${CANARY_ICON[r.verdict]} ${r.verdict.toUpperCase()} ` +
|
|
560
|
+
`| ${r.exit_code === null ? '—' : String(r.exit_code)} | ${r.message} |`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
lines.push('', '## Cross-check against the scan-scope census', '');
|
|
564
|
+
if (disagreements.length === 0) {
|
|
565
|
+
lines.push('No disagreement: every censused gate with a recipe went red, and every red gate');
|
|
566
|
+
lines.push('has a non-zero census count.');
|
|
567
|
+
} else {
|
|
568
|
+
lines.push('| Gate | Kind | Detail |', '|---|---|---|');
|
|
569
|
+
for (const d of disagreements) lines.push(`| \`${d.gate}\` | **${d.kind}** | ${d.detail} |`);
|
|
570
|
+
}
|
|
571
|
+
lines.push('');
|
|
572
|
+
return lines.join('\n');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Count of gate-shaped scripts, for the honest denominator in the ledger. */
|
|
576
|
+
export function count_gate_scripts(dir = path.join(REPO_ROOT, 'src/scripts')): number {
|
|
577
|
+
try {
|
|
578
|
+
return fs
|
|
579
|
+
.readdirSync(dir)
|
|
580
|
+
.filter((f) => /^(lint|check|audit)_.*\.ts$/.test(f) && !f.endsWith('.d.ts')).length;
|
|
581
|
+
} catch {
|
|
582
|
+
return 0;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function run_canary_mode(specs: readonly GateSpec[], argv: readonly string[]): number {
|
|
587
|
+
const results = specs.map((s) => run_canary(s));
|
|
588
|
+
const censusMd = fs.existsSync(CENSUS) ? fs.readFileSync(CENSUS, 'utf8') : '';
|
|
589
|
+
const disagreements = cross_check(results, parse_census(censusMd));
|
|
590
|
+
|
|
591
|
+
for (const r of results) {
|
|
592
|
+
process.stdout.write(` ${CANARY_ICON[r.verdict]} ${r.id}: ${r.message}\n`);
|
|
593
|
+
}
|
|
594
|
+
for (const d of disagreements) {
|
|
595
|
+
process.stdout.write(` ❌ DISAGREEMENT [${d.kind}] ${d.gate}: ${d.detail}\n`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const cycleId = `gate-surface-${new Date().toISOString().slice(0, 10)}`;
|
|
599
|
+
const li = argv.indexOf('--ledger');
|
|
600
|
+
if (li >= 0 && argv[li + 1] !== undefined) {
|
|
601
|
+
const out = path.resolve(REPO_ROOT, argv[li + 1] as string);
|
|
602
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
603
|
+
fs.writeFileSync(out, render_ledger(results, disagreements, count_gate_scripts(), cycleId), 'utf8');
|
|
604
|
+
process.stdout.write(`\nledger: ${path.relative(REPO_ROOT, out)}\n`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const recipes = results.filter((r) => r.verdict !== 'no_recipe').length;
|
|
608
|
+
const green = results.filter((r) => r.verdict === 'green' || r.verdict === 'crashed').length;
|
|
609
|
+
process.stdout.write(
|
|
610
|
+
`\nscanned: ${String(results.length)}\n` +
|
|
611
|
+
` recipes ${String(recipes)} · unproven ${String(results.length - recipes)} · ` +
|
|
612
|
+
`green(dead) ${String(green)} · disagreements ${String(disagreements.length)}\n`,
|
|
613
|
+
);
|
|
614
|
+
if (green > 0 || disagreements.length > 0) {
|
|
615
|
+
process.stdout.write('❌ canary found dead gate(s) or a census disagreement — each row is a defect ticket.\n');
|
|
616
|
+
return 1;
|
|
617
|
+
}
|
|
618
|
+
process.stdout.write('✅ every gate with a recipe went red over its planted defect.\n');
|
|
619
|
+
return 0;
|
|
620
|
+
}
|
|
621
|
+
|
|
286
622
|
function _isCliEntry(): boolean {
|
|
287
623
|
if (process.argv[1] === undefined) return false;
|
|
288
624
|
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
@@ -35,7 +35,8 @@ const GENERATOR_OUTPUT_ROOTS = [
|
|
|
35
35
|
{ root: '.claude/rules', generator: 'generate_rule_symlinks', tool: 'claude-code' },
|
|
36
36
|
{ root: '.claude/personas', generator: 'generate_persona_symlinks', tool: 'claude-code' },
|
|
37
37
|
{ root: '.claude/user-types', generator: 'generate_user_type_symlinks', tool: 'claude-code' },
|
|
38
|
-
{ root: '.claude/skills', generator: 'generate_claude_skills + generate_plugin_command_skills', tool: 'claude-code' },
|
|
38
|
+
{ root: '.claude/skills', generator: 'generate_claude_skills + generate_plugin_command_skills + generate_claude_commands', tool: 'claude-code' },
|
|
39
|
+
{ root: '.claude/commands', generator: 'generate_claude_project_commands', tool: 'claude-code' },
|
|
39
40
|
{ root: '.claude/agents', generator: 'generate_claude_subagents', tool: 'claude-code' },
|
|
40
41
|
{ root: '.augment', generator: 'sync_augment (condense main)', tool: 'augment' },
|
|
41
42
|
{ root: '.cursor/rules', generator: 'generate_cursor_mdc_rules', tool: 'cursor' },
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Ported from the retired Python `src/scripts/check_iron_law_prominence.py` (ADR-200,
|
|
7
7
|
* Phase 4 / Wave 4c). The CLI contract is pinned — positional
|
|
8
|
-
* paths (default
|
|
8
|
+
* paths (default `src/rules`), `--format` / `--quiet`
|
|
9
9
|
* flags, exit codes (0 clean, 1 violations, 3 internal error),
|
|
10
10
|
* stdout/stderr split, byte-identical finding messages, and the same
|
|
11
11
|
* heading parse / code-fence skipping. No behaviour changes — latent bugs
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* historical quirks preserved (consumers pin the exact behaviour).
|
|
12
12
|
*
|
|
13
13
|
* Fails when a single PR (or commit range) modifies more than one
|
|
14
|
-
* kernel rule under
|
|
14
|
+
* kernel rule under `src/rules/`. Override via the
|
|
15
15
|
* PR label `bundled-always-rules-acknowledged`.
|
|
16
16
|
*
|
|
17
17
|
* Kernel set is the locked 9-rule list in
|
|
@@ -45,12 +45,20 @@ import * as fs from 'node:fs';
|
|
|
45
45
|
import * as path from 'node:path';
|
|
46
46
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
47
47
|
|
|
48
|
+
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
49
|
+
|
|
48
50
|
const LEGACY = '.agent-src.uncondensed/';
|
|
49
51
|
const EXEMPT: ReadonlySet<string> = new Set([
|
|
50
52
|
'src/scripts/_lib/agent_src.py',
|
|
51
53
|
'src/scripts/check_references.py',
|
|
52
54
|
'src/scripts/check_condensed_paths.py',
|
|
53
55
|
'src/scripts/check_no_new_legacy_path.py', // this file documents the literal
|
|
56
|
+
// The `.ts` twin needs its own entry now: the docstring above says the
|
|
57
|
+
// faithful-twin rule covers it, but that rule reads a `.py` sibling from
|
|
58
|
+
// disk and the py2ts final deletion removed every sibling — so since then
|
|
59
|
+
// ANY edit to this guard's own message strings flags the guard. Surfaced
|
|
60
|
+
// by the Phase-5 edit that added the existing-literal pass below.
|
|
61
|
+
'src/scripts/check_no_new_legacy_path.ts',
|
|
54
62
|
'src/scripts/check_source_pointer_freshness.ts', // forbidden-substring detector for the legacy tree
|
|
55
63
|
]);
|
|
56
64
|
|
|
@@ -131,6 +139,154 @@ function find_offenders(diffText: string, twinCheck: TwinCheck = _is_faithful_tw
|
|
|
131
139
|
return offenders;
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Pass 2 — EXISTING hardcoded legacy scan roots (ratcheted).
|
|
144
|
+
//
|
|
145
|
+
// The diff pass above stops the debt from growing. It is structurally blind to
|
|
146
|
+
// the debt that is already there — and every one of the 14 dead gates found in
|
|
147
|
+
// 2026-07 was pre-existing, so a new-violations-only check could never have
|
|
148
|
+
// seen them (road-to-gates-that-can-fail Phase 5).
|
|
149
|
+
//
|
|
150
|
+
// WHY THIS IS NOT THE FULL-TREE LINT THAT WAS ALREADY REJECTED. The note in
|
|
151
|
+
// `find_offenders` records a measured rejection: a full-tree lint over `tests/`
|
|
152
|
+
// hits 44 files / 213 lines, most of them legitimate (the legacy detectors'
|
|
153
|
+
// own test data, `validator_ignore` substrings, synthetic tmpdir fixtures), so
|
|
154
|
+
// the signal drowns. That measurement stands and this pass does not repeat it.
|
|
155
|
+
// Three things make this scope different:
|
|
156
|
+
//
|
|
157
|
+
// 1. It reads EXECUTABLE code only — `src/scripts/**/*.{ts,mts,mjs}`. No
|
|
158
|
+
// prose, no docs, no tests, no fixtures.
|
|
159
|
+
// 2. It counts only lines that CONSTRUCT A PATH — the literal must reach
|
|
160
|
+
// `path.join(...)` inside a string. A detector holding the literal as
|
|
161
|
+
// data to match, strip, or print is not a scan root and is not counted.
|
|
162
|
+
// Measured on 2026-08-02: 236 raw mentions in src/scripts → 147 string
|
|
163
|
+
// literals → 70 path-constructing lines. The narrowing is the point.
|
|
164
|
+
// 3. It is a RATCHET, not a hard fail. The current count is committed to
|
|
165
|
+
// `src/config/gate-violation-baselines.json`; the gate goes red only when
|
|
166
|
+
// the count RISES. Every Phase-1 repair lowers it, and the baseline's
|
|
167
|
+
// 56-day expiry (see `_lib/gate_baseline.ts`) stops a stalled number from
|
|
168
|
+
// hardening into configuration.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/** Gate key in `src/config/gate-violation-baselines.json`. */
|
|
172
|
+
export const HARDCODED_ROOT_GATE = 'check_no_new_legacy_path:hardcoded-scan-roots';
|
|
173
|
+
|
|
174
|
+
/** The executable-code root this pass reads. */
|
|
175
|
+
export const HARDCODED_ROOT_SCAN_DIR = 'src/scripts';
|
|
176
|
+
|
|
177
|
+
/** Bare directory name — the form that appears inside a `path.join(...)` call. */
|
|
178
|
+
const LEGACY_DIRNAME = LEGACY.replace(/\/+$/, '');
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Files allowed to construct a legacy path forever.
|
|
182
|
+
*
|
|
183
|
+
* `_lib/agent_src.ts` is the shared resolver — owning the constant is its whole
|
|
184
|
+
* job, and every gate that routes through it survived the ADR-051 move. Exempting
|
|
185
|
+
* it is what makes the count mean "gates that bypassed the resolver".
|
|
186
|
+
*/
|
|
187
|
+
const HARDCODED_ROOT_EXEMPT: ReadonlySet<string> = new Set(['src/scripts/_lib/agent_src.ts']);
|
|
188
|
+
|
|
189
|
+
const _STRING_LITERAL_LEGACY = new RegExp(
|
|
190
|
+
`['"\`][^'"\`]*${LEGACY_DIRNAME.replace(/\./g, '\\.')}`,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
function _isCommentLine(trimmed: string): boolean {
|
|
194
|
+
return (
|
|
195
|
+
trimmed.startsWith('//') ||
|
|
196
|
+
trimmed.startsWith('*') ||
|
|
197
|
+
trimmed.startsWith('/*') ||
|
|
198
|
+
trimmed.startsWith('#')
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function* _walkScripts(dir: string): Generator<string> {
|
|
203
|
+
let entries: fs.Dirent[];
|
|
204
|
+
try {
|
|
205
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const e of entries) {
|
|
210
|
+
const full = path.join(dir, e.name);
|
|
211
|
+
if (e.isDirectory()) {
|
|
212
|
+
yield* _walkScripts(full);
|
|
213
|
+
} else if (/\.(ts|mts|mjs)$/.test(e.name) && !/\.d\.m?ts$/.test(e.name)) {
|
|
214
|
+
yield full;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Lines under `src/scripts/` that build a filesystem path against the retired
|
|
221
|
+
* tree. Returns `file:line: <source>` strings, sorted by path.
|
|
222
|
+
*
|
|
223
|
+
* `repoRoot` defaults to cwd (CI runs the guard at the repo root) and is
|
|
224
|
+
* injectable so tests can point at a fixture tree.
|
|
225
|
+
*/
|
|
226
|
+
export function find_hardcoded_scan_roots(
|
|
227
|
+
repoRoot: string = process.cwd(),
|
|
228
|
+
twinCheck: TwinCheck = _is_faithful_twin,
|
|
229
|
+
): string[] {
|
|
230
|
+
const root = path.join(repoRoot, HARDCODED_ROOT_SCAN_DIR);
|
|
231
|
+
const findings: string[] = [];
|
|
232
|
+
for (const abs of _walkScripts(root)) {
|
|
233
|
+
const rel = path.relative(repoRoot, abs).split(path.sep).join('/');
|
|
234
|
+
if (EXEMPT.has(rel) || HARDCODED_ROOT_EXEMPT.has(rel) || twinCheck(rel)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
let text: string;
|
|
238
|
+
try {
|
|
239
|
+
text = fs.readFileSync(abs, 'utf-8');
|
|
240
|
+
} catch {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!text.includes(LEGACY_DIRNAME)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
text.split('\n').forEach((line, i) => {
|
|
247
|
+
if (!line.includes(LEGACY_DIRNAME)) return;
|
|
248
|
+
const trimmed = line.trim();
|
|
249
|
+
if (_isCommentLine(trimmed)) return;
|
|
250
|
+
if (!/path\.join\s*\(/.test(line)) return;
|
|
251
|
+
if (!_STRING_LITERAL_LEGACY.test(line)) return;
|
|
252
|
+
findings.push(`${rel}:${i + 1}: ${trimmed.slice(0, 100)}`);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
findings.sort();
|
|
256
|
+
return findings;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Run the existing-literal pass and judge it against the committed baseline.
|
|
261
|
+
* Returns 0 when the count is at or below the baseline (and the baseline is
|
|
262
|
+
* fresh), 1 otherwise. Prints either way — a passing ratchet still reports the
|
|
263
|
+
* live number, which is the whole point of committing it.
|
|
264
|
+
*/
|
|
265
|
+
export function checkHardcodedScanRoots(repoRoot: string = process.cwd()): number {
|
|
266
|
+
const findings = find_hardcoded_scan_roots(repoRoot);
|
|
267
|
+
const verdict = checkRatchet({
|
|
268
|
+
gate: HARDCODED_ROOT_GATE,
|
|
269
|
+
actual: findings.length,
|
|
270
|
+
repoRoot,
|
|
271
|
+
});
|
|
272
|
+
if (verdict.ok) {
|
|
273
|
+
process.stdout.write(`✅ ${verdict.message}\n`);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
process.stdout.write(
|
|
277
|
+
`❌ Hardcoded \`${LEGACY}\` scan root(s) under ${HARDCODED_ROOT_SCAN_DIR}/ ` +
|
|
278
|
+
'above the recorded baseline:\n',
|
|
279
|
+
);
|
|
280
|
+
for (const f of findings.slice(0, 20)) {
|
|
281
|
+
process.stdout.write(` 🔴 ${f}\n`);
|
|
282
|
+
}
|
|
283
|
+
if (findings.length > 20) {
|
|
284
|
+
process.stdout.write(` … and ${findings.length - 20} more\n`);
|
|
285
|
+
}
|
|
286
|
+
process.stdout.write(`\n${verdict.message}\n`);
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
134
290
|
function main(): number {
|
|
135
291
|
// --stdin: read a unified diff from stdin (CI pipes `gh pr diff` — auth-safe,
|
|
136
292
|
// no `git fetch <base>` extraheader race on shallow PR-merge checkouts, the
|
|
@@ -170,6 +326,7 @@ function main(): number {
|
|
|
170
326
|
offenders = find_offenders((proc.stdout as string | null) ?? '');
|
|
171
327
|
}
|
|
172
328
|
|
|
329
|
+
let rc = 0;
|
|
173
330
|
if (offenders.length) {
|
|
174
331
|
process.stdout.write(
|
|
175
332
|
'❌ New `.agent-src.uncondensed/` reference(s) added under src/ ' +
|
|
@@ -182,11 +339,18 @@ function main(): number {
|
|
|
182
339
|
'\nFix: reference the real `src/` target. Existing stale prose is ' +
|
|
183
340
|
'migrated opportunistically; do not ADD new dead-path references.\n',
|
|
184
341
|
);
|
|
185
|
-
|
|
342
|
+
rc = 1;
|
|
343
|
+
} else {
|
|
344
|
+
process.stdout.write('✅ No new `.agent-src.uncondensed/` references added under src/.\n');
|
|
186
345
|
}
|
|
187
346
|
|
|
188
|
-
|
|
189
|
-
|
|
347
|
+
// Pass 2 — the pre-existing hardcoded scan roots the diff pass cannot see.
|
|
348
|
+
// Skippable for the golden-parity harness, which compares this guard's
|
|
349
|
+
// diff behaviour against a pinned reference implementation.
|
|
350
|
+
if (!process.argv.includes('--no-existing')) {
|
|
351
|
+
rc = Math.max(rc, checkHardcodedScanRoots());
|
|
352
|
+
}
|
|
353
|
+
return rc;
|
|
190
354
|
}
|
|
191
355
|
|
|
192
356
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -55,12 +55,26 @@ export interface PackSizeBudget {
|
|
|
55
55
|
/**
|
|
56
56
|
* Parse `npm pack --json` stdout. npm runs the `prepare` lifecycle script even
|
|
57
57
|
* under `--ignore-scripts` on some versions, and this repo's `prepare` prints a
|
|
58
|
-
* banner — straight into the stream we parse. Slice from
|
|
59
|
-
*
|
|
58
|
+
* banner — straight into the stream we parse. Slice from a `[` so a lifecycle
|
|
59
|
+
* banner is tolerated instead of becoming a SyntaxError.
|
|
60
|
+
*
|
|
61
|
+
* Not the FIRST `[`: npm echoes the lifecycle command line, and this repo's own
|
|
62
|
+
* `prepare` is `[ -d .git ] && bash src/scripts/install-hooks.sh || true` — its
|
|
63
|
+
* `[` precedes the payload's. Try each candidate offset in order and keep the
|
|
64
|
+
* first that actually parses (road-to-gates-that-can-fail Phase 6.2).
|
|
60
65
|
*/
|
|
61
66
|
export function parsePackJson(stdout: string): PackResult {
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
let parsed: PackResult[] | null = null;
|
|
68
|
+
for (let i = stdout.indexOf('['); i >= 0; i = stdout.indexOf('[', i + 1)) {
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(stdout.slice(i)) as PackResult[];
|
|
71
|
+
break;
|
|
72
|
+
} catch {
|
|
73
|
+
// Not the payload — a banner bracket. Keep scanning.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// No candidate parsed: reproduce the original SyntaxError from the raw text.
|
|
77
|
+
parsed ??= JSON.parse(stdout) as PackResult[];
|
|
64
78
|
const first = parsed[0];
|
|
65
79
|
if (first === undefined) throw new Error('npm pack --json returned an empty array');
|
|
66
80
|
return first;
|