@event4u/agent-config 14.17.0 → 14.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -1,28 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
/**
|
|
3
|
-
* CI guard for README.md
|
|
3
|
+
* CI guard for README.md size and time-to-first-command.
|
|
4
4
|
*
|
|
5
5
|
* Ported from the retired Python `src/scripts/lint_readme_size.py` (ADR-200, Phase 4 /
|
|
6
6
|
* Wave 4b). The CLI contract is pinned — `--quiet` flag, exit
|
|
7
|
-
* codes (0 within budget, 1 over budget / missing), stdout/stderr split
|
|
8
|
-
* byte-identical finding messages.
|
|
7
|
+
* codes (0 within budget, 1 over budget / missing), stdout/stderr split.
|
|
9
8
|
*
|
|
10
9
|
* The role-first-onboarding roadmap (Phase 2 Step 6) freezes README at
|
|
11
10
|
* its current length: replace, do not grow. Every line added above the
|
|
12
|
-
* fold must displace an existing line.
|
|
11
|
+
* fold must displace an existing line.
|
|
12
|
+
*
|
|
13
|
+
* Three budgets, because a line budget alone missed a full regression of this
|
|
14
|
+
* file once already — see WORD_LIMIT below for the measurement and the reason
|
|
15
|
+
* the two newer numbers are not derived from the current file.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import * as fs from 'node:fs';
|
|
19
|
+
import * as os from 'node:os';
|
|
16
20
|
import * as path from 'node:path';
|
|
17
21
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
22
|
|
|
19
|
-
import {
|
|
23
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
24
|
+
import { assertWatchlistResolves, DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
20
25
|
|
|
21
26
|
const _HERE = fileURLToPath(import.meta.url);
|
|
22
27
|
|
|
23
28
|
const README = 'README.md';
|
|
24
29
|
const LIMIT = 750;
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Word and first-command budgets, derived from the README as it stood when
|
|
33
|
+
* `readme-and-docs-improvement.md` archived — NOT from the current file.
|
|
34
|
+
*
|
|
35
|
+
* That roadmap reached this same goal and archived complete on 2026-05-18. The
|
|
36
|
+
* file then drifted back, and the line budget above could not see it: lines
|
|
37
|
+
* went 747 -> 638, which is *better*, while words went 4849 -> 5550 and the
|
|
38
|
+
* first executable command moved from line 75 to line 160. A guard measuring
|
|
39
|
+
* only lines stayed green through every step of that, with 112 lines to spare.
|
|
40
|
+
*
|
|
41
|
+
* **This is the third recorded arrival of the same complaint, and nothing in
|
|
42
|
+
* the repository caught any of them.** Two independent external readers raised
|
|
43
|
+
* it again on 2026-09-06, three months after the fix archived as complete. A
|
|
44
|
+
* budget nobody watches is how a fix stops holding, which is why these two
|
|
45
|
+
* numbers are registered in `src/config/gate-coverage.yml` rather than living
|
|
46
|
+
* only here: removing them is then a gate failure rather than a quiet edit.
|
|
47
|
+
*
|
|
48
|
+
* Deriving these two numbers from the current file would make the gate green
|
|
49
|
+
* on day one and ratchet the regression in as the new floor. They come from
|
|
50
|
+
* the archive commit instead, and this command re-derives both:
|
|
51
|
+
*
|
|
52
|
+
* git show 032a244a3:README.md > /tmp/r.md && \
|
|
53
|
+
* echo "words=$(wc -w < /tmp/r.md)" \
|
|
54
|
+
* "first_fence=$(grep -n '^```' /tmp/r.md | head -1 | cut -d: -f1)"
|
|
55
|
+
* # words=4849 first_fence=75
|
|
56
|
+
*/
|
|
57
|
+
const WORD_LIMIT = 4849;
|
|
58
|
+
const FIRST_COMMAND_LIMIT = 75;
|
|
59
|
+
|
|
26
60
|
/**
|
|
27
61
|
* Count splitlines() entries the way Python does — terminal `\n` does NOT
|
|
28
62
|
* yield a trailing empty entry. Universal-newline aware (\r\n / \r / \n).
|
|
@@ -46,7 +80,10 @@ function _splitlinesCount(text: string): number {
|
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
function main(): number {
|
|
83
|
+
if (process.argv.slice(2).includes('--self-test')) return selfTest();
|
|
49
84
|
const quiet = process.argv.slice(2).includes('--quiet');
|
|
85
|
+
const rootArgIdx = process.argv.indexOf('--root');
|
|
86
|
+
const root = rootArgIdx === -1 ? process.cwd() : (process.argv[rootArgIdx + 1] ?? process.cwd());
|
|
50
87
|
// README is this gate's whole scope and resolves against cwd, so a wrong
|
|
51
88
|
// cwd is indistinguishable from a deleted file. Replaces the bare
|
|
52
89
|
// existsSync check: same exit code, but the failure now names the root it
|
|
@@ -56,7 +93,7 @@ function main(): number {
|
|
|
56
93
|
assertWatchlistResolves({
|
|
57
94
|
gate: 'lint_readme_size',
|
|
58
95
|
candidates: [README],
|
|
59
|
-
repoRoot:
|
|
96
|
+
repoRoot: root,
|
|
60
97
|
});
|
|
61
98
|
} catch (exc) {
|
|
62
99
|
if (exc instanceof DeadScopeError) {
|
|
@@ -67,20 +104,129 @@ function main(): number {
|
|
|
67
104
|
}
|
|
68
105
|
throw exc;
|
|
69
106
|
}
|
|
70
|
-
|
|
107
|
+
reportScanned({
|
|
108
|
+
gate: 'lint_readme_size',
|
|
109
|
+
scanned: 1,
|
|
110
|
+
units: 'file',
|
|
111
|
+
roots: [README],
|
|
112
|
+
});
|
|
113
|
+
const text = fs.readFileSync(path.join(root, README), 'utf-8');
|
|
71
114
|
const n = _splitlinesCount(text);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
const words = countWords(text);
|
|
116
|
+
const firstCommand = firstCommandLine(text);
|
|
117
|
+
|
|
118
|
+
// All three figures print on both paths. The regression this gate missed
|
|
119
|
+
// was visible in two numbers nobody was shown, so reporting only the one
|
|
120
|
+
// that failed would rebuild the blind spot one measurement further along.
|
|
121
|
+
const report =
|
|
122
|
+
`${README}: ${String(n)} lines (limit ${String(LIMIT)}) · ` +
|
|
123
|
+
`${String(words)} words (limit ${String(WORD_LIMIT)}) · ` +
|
|
124
|
+
`first command at line ${firstCommand === null ? 'none' : String(firstCommand)} ` +
|
|
125
|
+
`(limit ${String(FIRST_COMMAND_LIMIT)})`;
|
|
126
|
+
|
|
127
|
+
const failures: string[] = [];
|
|
128
|
+
if (n > LIMIT) failures.push(`${String(n)} lines exceeds ${String(LIMIT)}`);
|
|
129
|
+
if (words > WORD_LIMIT) {
|
|
130
|
+
failures.push(`${String(words)} words exceeds ${String(WORD_LIMIT)}`);
|
|
131
|
+
}
|
|
132
|
+
if (firstCommand === null) {
|
|
133
|
+
failures.push('no fenced command block at all');
|
|
134
|
+
} else if (firstCommand > FIRST_COMMAND_LIMIT) {
|
|
135
|
+
failures.push(
|
|
136
|
+
`first command at line ${String(firstCommand)} exceeds ${String(FIRST_COMMAND_LIMIT)}`,
|
|
75
137
|
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (failures.length > 0) {
|
|
141
|
+
process.stdout.write(`FAIL ${report}\n`);
|
|
142
|
+
for (const f of failures) {
|
|
143
|
+
process.stdout.write(` - ${f}\n`);
|
|
144
|
+
}
|
|
145
|
+
process.stdout.write(' Trim before merge.\n');
|
|
76
146
|
return 1;
|
|
77
147
|
}
|
|
78
148
|
if (!quiet) {
|
|
79
|
-
process.stdout.write(`OK ${
|
|
149
|
+
process.stdout.write(`OK ${report}\n`);
|
|
80
150
|
}
|
|
81
151
|
return 0;
|
|
82
152
|
}
|
|
83
153
|
|
|
154
|
+
/** Whitespace-separated tokens, matching `wc -w` (which the budget derives from). */
|
|
155
|
+
function countWords(text: string): number {
|
|
156
|
+
const trimmed = text.trim();
|
|
157
|
+
return trimmed === '' ? 0 : trimmed.split(/\s+/).length;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 1-based line of the first fenced block, or `null` when there is none.
|
|
162
|
+
*
|
|
163
|
+
* The opening fence is what a reader's eye reaches, so the fence line itself
|
|
164
|
+
* is the measurement — not the first line of code inside it.
|
|
165
|
+
*/
|
|
166
|
+
function firstCommandLine(text: string): number | null {
|
|
167
|
+
const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
168
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
169
|
+
if (lines[i]!.startsWith('```')) return i + 1;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The gate proving it still DISCRIMINATES, not merely that it ran.
|
|
176
|
+
*
|
|
177
|
+
* One rejecting case per budget, because the failure this gate exists to end
|
|
178
|
+
* was a guard that measured one of the three and stayed green while the other
|
|
179
|
+
* two regressed. A suite that only exercises the line budget would reproduce
|
|
180
|
+
* that blind spot exactly.
|
|
181
|
+
*/
|
|
182
|
+
function selfTest(): number {
|
|
183
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'readme-selftest-'));
|
|
184
|
+
const write = (name: string, body: string): string => {
|
|
185
|
+
const dir = path.join(tmp, name);
|
|
186
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
187
|
+
fs.writeFileSync(path.join(dir, README), body, 'utf-8');
|
|
188
|
+
return dir;
|
|
189
|
+
};
|
|
190
|
+
const run = (root: string): number =>
|
|
191
|
+
runGateCli(process.cwd(), 'src/scripts/lint_readme_size.ts', ['--root', root], root);
|
|
192
|
+
|
|
193
|
+
const fence = '```bash\nnpx thing\n```\n';
|
|
194
|
+
const filler = (n: number): string => `${'word '.repeat(n).trim()}\n`;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
return runSelfTest({
|
|
198
|
+
gate: 'lint_readme_size',
|
|
199
|
+
minCases: 4,
|
|
200
|
+
minRejectCases: 3,
|
|
201
|
+
cases: [
|
|
202
|
+
{
|
|
203
|
+
name: 'a README whose first command sits below the budget is rejected',
|
|
204
|
+
expect: 'reject',
|
|
205
|
+
run: () =>
|
|
206
|
+
run(write('late-command', `${'filler\n'.repeat(FIRST_COMMAND_LIMIT + 5)}${fence}`)),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'a README over the word budget is rejected',
|
|
210
|
+
expect: 'reject',
|
|
211
|
+
run: () => run(write('wordy', `${fence}${filler(WORD_LIMIT + 50)}`)),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'a README over the line budget is rejected',
|
|
215
|
+
expect: 'reject',
|
|
216
|
+
run: () => run(write('long', `${fence}${'x\n'.repeat(LIMIT + 5)}`)),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'a short README with an early command passes',
|
|
220
|
+
expect: 'accept',
|
|
221
|
+
run: () => run(write('good', `# Title\n\n${fence}`)),
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
} finally {
|
|
226
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
84
230
|
function _isCliEntry(): boolean {
|
|
85
231
|
if (process.argv[1] === undefined) {
|
|
86
232
|
return false;
|
|
@@ -107,4 +253,4 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
107
253
|
process.exit(main());
|
|
108
254
|
}
|
|
109
255
|
|
|
110
|
-
export { README, LIMIT, main };
|
|
256
|
+
export { README, LIMIT, WORD_LIMIT, FIRST_COMMAND_LIMIT, countWords, firstCommandLine, main };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Structural validator for the RDP trigger-fixture corpus.
|
|
4
|
+
*
|
|
5
|
+
* `tests/reasoning-layer-eval/` shipped 21 fixtures and, after the Python
|
|
6
|
+
* retirement, zero validators: `find … -name '*.ts' -o -name '*.py'` returned
|
|
7
|
+
* nothing while the corpus's own README and `trigger-fixtures.json`'s
|
|
8
|
+
* `description` still told a reader to run two deleted files. A corpus nothing
|
|
9
|
+
* can check is a corpus nothing does check.
|
|
10
|
+
*
|
|
11
|
+
* Scope is deliberately the SHAPE and nothing else. Live scoring against a model
|
|
12
|
+
* is `skill_trigger_eval.ts`, which is billable and already exists; nothing about
|
|
13
|
+
* validating the corpus needs a model call. What the deleted `validate_fixtures.py`
|
|
14
|
+
* ALSO did and this does not is check the cost-gating invariants — that half was
|
|
15
|
+
* not ported and is not claimed here.
|
|
16
|
+
*
|
|
17
|
+
* Exit codes: 0 corpus well-formed · 1 a row is malformed · 2 the corpus is
|
|
18
|
+
* missing or unparseable.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as os from 'node:os';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
26
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
27
|
+
|
|
28
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
29
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
30
|
+
const CORPUS_REL = 'tests/reasoning-layer-eval/trigger-fixtures.json';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The five keys a row carries, and no others.
|
|
34
|
+
*
|
|
35
|
+
* Exact-set rather than a minimum: an unknown key is how a schema drifts in a
|
|
36
|
+
* corpus with no reader, and the two additive fields this suite already has
|
|
37
|
+
* over the per-skill `evals/triggers.json` convention (`discipline`, `host`)
|
|
38
|
+
* are exactly what a permissive check would have let multiply.
|
|
39
|
+
*/
|
|
40
|
+
const REQUIRED_KEYS = ['q', 'trigger', 'discipline', 'host', 'note'] as const;
|
|
41
|
+
const HOSTS = ['standard', 'strong'] as const;
|
|
42
|
+
|
|
43
|
+
export interface Finding {
|
|
44
|
+
where: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateCorpus(text: string): { findings: Finding[]; rows: number } {
|
|
49
|
+
const findings: Finding[] = [];
|
|
50
|
+
let doc: unknown;
|
|
51
|
+
try {
|
|
52
|
+
doc = JSON.parse(text);
|
|
53
|
+
} catch (exc) {
|
|
54
|
+
return {
|
|
55
|
+
findings: [{ where: CORPUS_REL, reason: `not valid JSON: ${String(exc)}` }],
|
|
56
|
+
rows: 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const top = doc as Record<string, unknown>;
|
|
60
|
+
for (const k of ['suite', 'description', 'queries']) {
|
|
61
|
+
if (top[k] === undefined) {
|
|
62
|
+
findings.push({ where: CORPUS_REL, reason: `top level is missing '${k}'` });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const queries = Array.isArray(top['queries']) ? (top['queries'] as unknown[]) : [];
|
|
66
|
+
if (queries.length === 0) {
|
|
67
|
+
findings.push({ where: CORPUS_REL, reason: "'queries' is empty or not an array" });
|
|
68
|
+
return { findings, rows: 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
queries.forEach((raw, i) => {
|
|
72
|
+
const where = `${CORPUS_REL}[${String(i)}]`;
|
|
73
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
74
|
+
findings.push({ where, reason: 'row is not an object' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const row = raw as Record<string, unknown>;
|
|
78
|
+
const keys = Object.keys(row).sort();
|
|
79
|
+
const expected: string[] = [...REQUIRED_KEYS].sort();
|
|
80
|
+
for (const k of expected) {
|
|
81
|
+
if (!keys.includes(k)) findings.push({ where, reason: `missing key '${k}'` });
|
|
82
|
+
}
|
|
83
|
+
for (const k of keys) {
|
|
84
|
+
if (!expected.includes(k)) findings.push({ where, reason: `unknown key '${k}'` });
|
|
85
|
+
}
|
|
86
|
+
if (row['q'] !== undefined && (typeof row['q'] !== 'string' || row['q'].trim() === '')) {
|
|
87
|
+
findings.push({ where, reason: "'q' must be a non-empty string" });
|
|
88
|
+
}
|
|
89
|
+
if (row['note'] !== undefined && (typeof row['note'] !== 'string' || row['note'].trim() === '')) {
|
|
90
|
+
findings.push({ where, reason: "'note' must be a non-empty string" });
|
|
91
|
+
}
|
|
92
|
+
if (row['discipline'] !== undefined && (typeof row['discipline'] !== 'string' || row['discipline'].trim() === '')) {
|
|
93
|
+
findings.push({ where, reason: "'discipline' must be a non-empty string" });
|
|
94
|
+
}
|
|
95
|
+
if (row['trigger'] !== undefined && typeof row['trigger'] !== 'boolean') {
|
|
96
|
+
findings.push({ where, reason: "'trigger' must be a boolean, not a string" });
|
|
97
|
+
}
|
|
98
|
+
if (row['host'] !== undefined && !(HOSTS as readonly unknown[]).includes(row['host'])) {
|
|
99
|
+
findings.push({
|
|
100
|
+
where,
|
|
101
|
+
reason: `'host' must be one of ${HOSTS.join('|')}, got ${JSON.stringify(row['host'])}`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return { findings, rows: queries.length };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function main(): number {
|
|
109
|
+
if (process.argv.slice(2).includes('--self-test')) return selfTest();
|
|
110
|
+
const rootIdx = process.argv.indexOf('--root');
|
|
111
|
+
const root = rootIdx === -1 ? ROOT : (process.argv[rootIdx + 1] ?? ROOT);
|
|
112
|
+
const file = path.join(root, CORPUS_REL);
|
|
113
|
+
if (!fs.existsSync(file)) {
|
|
114
|
+
process.stderr.write(`❌ corpus not found: ${CORPUS_REL} (under ${root})\n`);
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
|
117
|
+
const { findings, rows } = validateCorpus(fs.readFileSync(file, 'utf-8'));
|
|
118
|
+
// Emitted on the red path too: a gate that reports what it inspected only
|
|
119
|
+
// when it passes leaves the coverage census blind exactly when it matters.
|
|
120
|
+
reportScanned({
|
|
121
|
+
gate: 'lint_reasoning_fixtures',
|
|
122
|
+
scanned: rows,
|
|
123
|
+
units: 'fixture row(s)',
|
|
124
|
+
roots: [CORPUS_REL],
|
|
125
|
+
});
|
|
126
|
+
if (findings.length > 0) {
|
|
127
|
+
for (const f of findings) {
|
|
128
|
+
process.stdout.write(`❌ ${f.where}: ${f.reason}\n`);
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(`\n${String(findings.length)} malformed fixture finding(s).\n`);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
process.stdout.write(
|
|
134
|
+
`✅ lint_reasoning_fixtures: ${String(rows)} fixture row(s) well-formed ` +
|
|
135
|
+
`(${REQUIRED_KEYS.join(', ')}; host ∈ ${HOSTS.join('|')}).\n`,
|
|
136
|
+
);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The gate proving it still DISCRIMINATES — one rejecting case per rule the roadmap names. */
|
|
141
|
+
function selfTest(): number {
|
|
142
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'rdp-fixtures-selftest-'));
|
|
143
|
+
const good = {
|
|
144
|
+
q: 'a question',
|
|
145
|
+
trigger: true,
|
|
146
|
+
discipline: 'grounding',
|
|
147
|
+
host: 'standard',
|
|
148
|
+
note: 'why',
|
|
149
|
+
};
|
|
150
|
+
const write = (name: string, queries: unknown[]): string => {
|
|
151
|
+
const dir = path.join(tmp, name, 'tests', 'reasoning-layer-eval');
|
|
152
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
153
|
+
fs.writeFileSync(
|
|
154
|
+
path.join(dir, 'trigger-fixtures.json'),
|
|
155
|
+
JSON.stringify({ suite: 's', description: 'd', queries }),
|
|
156
|
+
'utf-8',
|
|
157
|
+
);
|
|
158
|
+
return path.join(tmp, name);
|
|
159
|
+
};
|
|
160
|
+
const run = (root: string): number =>
|
|
161
|
+
runGateCli(ROOT, 'src/scripts/lint_reasoning_fixtures.ts', ['--root', root], root);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
return runSelfTest({
|
|
165
|
+
gate: 'lint_reasoning_fixtures',
|
|
166
|
+
minCases: 4,
|
|
167
|
+
minRejectCases: 3,
|
|
168
|
+
cases: [
|
|
169
|
+
{
|
|
170
|
+
name: 'a row with a sixth key is rejected',
|
|
171
|
+
expect: 'reject',
|
|
172
|
+
run: () => run(write('sixth-key', [{ ...good, extra: 1 }])),
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'a row missing discipline is rejected',
|
|
176
|
+
expect: 'reject',
|
|
177
|
+
run: () => {
|
|
178
|
+
const { discipline: _drop, ...rest } = good;
|
|
179
|
+
return run(write('no-discipline', [rest]));
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'host: "medium" is rejected',
|
|
184
|
+
expect: 'reject',
|
|
185
|
+
run: () => run(write('bad-host', [{ ...good, host: 'medium' }])),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'a well-formed row passes',
|
|
189
|
+
expect: 'accept',
|
|
190
|
+
run: () => run(write('good', [good])),
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
} finally {
|
|
195
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _isCliEntry(): boolean {
|
|
200
|
+
if (process.argv[1] === undefined) return false;
|
|
201
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
202
|
+
try {
|
|
203
|
+
return (
|
|
204
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
205
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
206
|
+
);
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
213
|
+
process.exit(main());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { CORPUS_REL, REQUIRED_KEYS, HOSTS, main };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* A skill may not say "explicit request only" and publish auto-trigger keywords.
|
|
4
|
+
*
|
|
5
|
+
* The description is consumed — it is the frontmatter a catalogue and a host
|
|
6
|
+
* surface read. The keyword section is not: measured across the tree, nothing
|
|
7
|
+
* in `src/` parses `## Auto-trigger keywords`, so a skill carrying both halves
|
|
8
|
+
* is not mis-ROUTED, it is mis-DESCRIBED. A reader deciding whether they may
|
|
9
|
+
* invoke the skill meets two opposite answers in one file, and the half that
|
|
10
|
+
* looks operational is the half nothing reads.
|
|
11
|
+
*
|
|
12
|
+
* Three skills shipped in that state and a human had to notice it. The pair is
|
|
13
|
+
* mechanically decidable from the file, so noticing it again should not be
|
|
14
|
+
* anybody's job.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not a judgement about WHICH half is right. Both are legitimate
|
|
17
|
+
* designs; only the contradiction is a defect, and which way to resolve it is
|
|
18
|
+
* the author's call.
|
|
19
|
+
*
|
|
20
|
+
* Exit codes: 0 no contradiction · 1 at least one file carries both.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import * as os from 'node:os';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
+
|
|
27
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
28
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
29
|
+
|
|
30
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
31
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
32
|
+
const SKILLS_REL = 'src/skills';
|
|
33
|
+
|
|
34
|
+
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
35
|
+
/** Matched in the description only — the phrase is meaningful there, prose elsewhere. */
|
|
36
|
+
const EXPLICIT_ONLY_RE = /explicit[- ]request[- ]only|explicit request only/i;
|
|
37
|
+
const AUTO_TRIGGER_RE = /^##\s+Auto-trigger keywords\s*$/m;
|
|
38
|
+
|
|
39
|
+
export function contradicts(text: string): boolean {
|
|
40
|
+
const fm = FRONTMATTER_RE.exec(text);
|
|
41
|
+
if (fm === null) return false;
|
|
42
|
+
// `description:` may be a folded or quoted scalar spanning lines, so the
|
|
43
|
+
// slice runs to the next top-level key rather than to the next newline.
|
|
44
|
+
const desc = /^description:([\s\S]*?)(?=\n[a-z_]+:|$)/im.exec(fm[1] as string);
|
|
45
|
+
if (desc === null) return false;
|
|
46
|
+
if (!EXPLICIT_ONLY_RE.test(desc[1] as string)) return false;
|
|
47
|
+
return AUTO_TRIGGER_RE.test(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function main(): number {
|
|
51
|
+
if (process.argv.slice(2).includes('--self-test')) return selfTest();
|
|
52
|
+
const rootIdx = process.argv.indexOf('--root');
|
|
53
|
+
const root = rootIdx === -1 ? ROOT : (process.argv[rootIdx + 1] ?? ROOT);
|
|
54
|
+
const dir = path.join(root, SKILLS_REL);
|
|
55
|
+
let names: string[] = [];
|
|
56
|
+
try {
|
|
57
|
+
names = fs.readdirSync(dir).sort();
|
|
58
|
+
} catch {
|
|
59
|
+
process.stderr.write(`❌ no skills directory under ${root}/${SKILLS_REL}\n`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const offenders: string[] = [];
|
|
63
|
+
let scanned = 0;
|
|
64
|
+
for (const n of names) {
|
|
65
|
+
const f = path.join(dir, n, 'SKILL.md');
|
|
66
|
+
if (!fs.existsSync(f)) continue;
|
|
67
|
+
scanned += 1;
|
|
68
|
+
if (contradicts(fs.readFileSync(f, 'utf-8'))) {
|
|
69
|
+
offenders.push(`${SKILLS_REL}/${n}/SKILL.md`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
reportScanned({
|
|
73
|
+
gate: 'lint_skill_trigger_contradiction',
|
|
74
|
+
scanned,
|
|
75
|
+
units: 'SKILL.md file(s)',
|
|
76
|
+
roots: [SKILLS_REL],
|
|
77
|
+
});
|
|
78
|
+
if (offenders.length > 0) {
|
|
79
|
+
for (const o of offenders) {
|
|
80
|
+
process.stdout.write(
|
|
81
|
+
`❌ ${o}: description says explicit-request-only AND the body publishes ` +
|
|
82
|
+
'`## Auto-trigger keywords` — the two halves say opposite things ' +
|
|
83
|
+
'and nothing in src/ parses the keyword half, so the operational-looking ' +
|
|
84
|
+
'one is the inert one.\n',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(
|
|
88
|
+
`\n${String(offenders.length)} contradicting skill(s). Resolve by removing whichever ` +
|
|
89
|
+
'half is not true; the check does not decide which.\n',
|
|
90
|
+
);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
`✅ lint_skill_trigger_contradiction: ${String(scanned)} skill(s) inspected, ` +
|
|
95
|
+
'none claims explicit-request-only while publishing auto-trigger keywords.\n',
|
|
96
|
+
);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function selfTest(): number {
|
|
101
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'trigger-contradiction-selftest-'));
|
|
102
|
+
const write = (name: string, body: string): string => {
|
|
103
|
+
const dir = path.join(tmp, name, SKILLS_REL, 'sample');
|
|
104
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
105
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), body, 'utf-8');
|
|
106
|
+
return path.join(tmp, name);
|
|
107
|
+
};
|
|
108
|
+
const run = (root: string): number =>
|
|
109
|
+
runGateCli(ROOT, 'src/scripts/lint_skill_trigger_contradiction.ts', ['--root', root], root);
|
|
110
|
+
|
|
111
|
+
const both =
|
|
112
|
+
'---\nname: sample\ndescription: Does a thing; explicit request only.\n---\n\n## Auto-trigger keywords\n\nfoo\n';
|
|
113
|
+
const descOnly = '---\nname: sample\ndescription: Does a thing; explicit request only.\n---\n\nbody\n';
|
|
114
|
+
const autoOnly = '---\nname: sample\ndescription: Does a thing.\n---\n\n## Auto-trigger keywords\n\nfoo\n';
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return runSelfTest({
|
|
118
|
+
gate: 'lint_skill_trigger_contradiction',
|
|
119
|
+
minCases: 3,
|
|
120
|
+
minRejectCases: 1,
|
|
121
|
+
cases: [
|
|
122
|
+
{
|
|
123
|
+
name: 'a skill carrying BOTH halves is rejected',
|
|
124
|
+
expect: 'reject',
|
|
125
|
+
run: () => run(write('both', both)),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'explicit-request-only with no keyword section passes',
|
|
129
|
+
expect: 'accept',
|
|
130
|
+
run: () => run(write('desc-only', descOnly)),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'auto-trigger keywords with no explicit-only claim passes',
|
|
134
|
+
expect: 'accept',
|
|
135
|
+
run: () => run(write('auto-only', autoOnly)),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
} finally {
|
|
140
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _isCliEntry(): boolean {
|
|
145
|
+
if (process.argv[1] === undefined) return false;
|
|
146
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
147
|
+
try {
|
|
148
|
+
return (
|
|
149
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
150
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
151
|
+
);
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
158
|
+
process.exit(main());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { SKILLS_REL, main };
|