@event4u/agent-config 9.12.0 → 9.14.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 -238
- package/README.md +1 -1
- package/dist/agent-src/commands/optimize/deep.md +147 -0
- package/dist/agent-src/commands/optimize.md +12 -6
- package/dist/agent-src/rules/active-remediation.md +0 -2
- package/dist/agent-src/rules/architecture.md +0 -1
- package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -2
- package/dist/agent-src/rules/broken-access-control.md +0 -2
- package/dist/agent-src/rules/code-comment-discipline.md +0 -3
- package/dist/agent-src/rules/code-provenance.md +0 -3
- package/dist/agent-src/rules/communication-through-line.md +2 -3
- package/dist/agent-src/rules/content-quoting-floor.md +0 -2
- package/dist/agent-src/rules/context-hygiene.md +1 -3
- package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
- package/dist/agent-src/rules/design-fidelity.md +29 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
- package/dist/agent-src/rules/downstream-changes.md +10 -1
- package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
- package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
- package/dist/agent-src/rules/git-history-discipline.md +0 -7
- package/dist/agent-src/rules/guidelines.md +0 -2
- package/dist/agent-src/rules/improve-before-implement.md +4 -2
- package/dist/agent-src/rules/invite-challenge.md +0 -4
- package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
- package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
- package/dist/agent-src/rules/missing-tool-handling.md +0 -1
- package/dist/agent-src/rules/no-attribution-footers.md +0 -3
- package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
- package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
- package/dist/agent-src/rules/no-roadmap-references.md +0 -2
- package/dist/agent-src/rules/output-discipline.md +0 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
- package/dist/agent-src/rules/preservation-guard.md +0 -3
- package/dist/agent-src/rules/role-mode-adherence.md +0 -1
- package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
- package/dist/agent-src/rules/session-canary.md +0 -2
- package/dist/agent-src/rules/size-enforcement.md +0 -5
- package/dist/agent-src/rules/source-confidentiality.md +0 -3
- package/dist/agent-src/rules/source-discovery-gate.md +0 -2
- package/dist/agent-src/rules/think-before-action.md +3 -3
- package/dist/agent-src/rules/token-budget-discipline.md +0 -1
- package/dist/agent-src/rules/token-efficiency.md +1 -2
- package/dist/agent-src/rules/tool-safety.md +0 -1
- package/dist/agent-src/rules/user-interaction.md +0 -3
- package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
- package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
- package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
- package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
- package/dist/agent-src/skills/design-review/SKILL.md +11 -0
- package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
- package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
- package/dist/agent-src/skills/performance/SKILL.md +14 -0
- package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
- package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
- package/dist/agent-src/skills/video-director/SKILL.md +9 -6
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +154 -13
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +40 -0
- package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-GCVFLBP2.js} +399 -365
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_prune.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 +123 -73
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +174 -13
- package/dist/install/install.mjs +648 -614
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +783 -507
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +2 -2
- package/docs/MIGRATION.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
- package/docs/guidelines/code-clarity.md +9 -2
- package/docs/guidelines/design-fidelity-mechanics.md +44 -0
- package/llms.txt +63 -27
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +154 -13
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
- package/src/config/agent-settings.template.yml +8 -0
- package/src/scripts/_lib/counted_probe.ts +142 -0
- package/src/scripts/_lib/exec_evidence.ts +5 -0
- package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
- package/src/scripts/_lib/release_scope.ts +207 -0
- package/src/scripts/_lib/scoped_projection.ts +212 -0
- package/src/scripts/ai_council/stance_tally.ts +37 -2
- package/src/scripts/audit_auto_rules.ts +9 -9
- package/src/scripts/audit_likelihood.ts +3 -3
- package/src/scripts/audit_overlap.ts +2 -2
- package/src/scripts/audit_skill_overlap.ts +233 -95
- package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
- package/src/scripts/build_proof.ts +15 -1
- package/src/scripts/check_artefact_count_messaging.ts +128 -15
- package/src/scripts/check_dependency_floors.ts +16 -6
- package/src/scripts/check_gate_coverage.ts +6 -6
- package/src/scripts/check_kernel_rule_bundle.ts +5 -4
- package/src/scripts/check_token_quality_golden.ts +0 -18
- package/src/scripts/code_graph/loader.ts +35 -6
- package/src/scripts/compile_router.ts +3 -8
- package/src/scripts/condense.ts +2 -2
- package/src/scripts/count_scoped_projection.ts +104 -0
- package/src/scripts/explain_run.ts +0 -1
- package/src/scripts/generate_pack_manifests.ts +3 -20
- package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
- package/src/scripts/hooks/block_no_verify.ts +101 -0
- package/src/scripts/install.ts +13 -90
- package/src/scripts/lint_agent_security.ts +5 -4
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_regression.ts +92 -1
- package/src/scripts/lint_skill_descriptions.ts +93 -5
- package/src/scripts/project_thin_rules.ts +2 -2
- package/src/scripts/release.ts +107 -3
- package/src/scripts/resolve_lint_scope.ts +53 -0
- package/src/scripts/router_telemetry.ts +0 -5
- package/src/scripts/runtime_dispatcher.ts +40 -0
- package/src/scripts/schemas/pack.schema.json +1 -5
- package/src/scripts/schemas/rule.schema.json +1 -2
- package/src/scripts/self_review_gate.ts +15 -73
- package/src/scripts/skill_linter.ts +160 -11
- package/src/scripts/sweep_dead_scan_roots.ts +550 -0
- package/src/scripts/trigger_coverage.ts +11 -37
- package/src/scripts/update_counts.ts +90 -11
|
@@ -44,11 +44,14 @@ const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..')
|
|
|
44
44
|
* exact pin cannot suffer the freshest-patch failure (the version is either
|
|
45
45
|
* cached or it is not), but it also forfeits patch fixes — so each one is
|
|
46
46
|
* named here rather than silently tolerated.
|
|
47
|
+
*
|
|
48
|
+
* Empty since the code-graph parser pair (`web-tree-sitter` /
|
|
49
|
+
* `tree-sitter-wasms`) left `dependencies` — it was the only entry. The pair's
|
|
50
|
+
* ABI pin now lives in `code_graph/loader.ts`'s install hint, because that is
|
|
51
|
+
* the only place a re-enabler reads it; this gate scans `dependencies` and so
|
|
52
|
+
* no longer sees the pair at all.
|
|
47
53
|
*/
|
|
48
|
-
export const EXACT_PIN_EXCEPTIONS: Readonly<Record<string, string>> = {
|
|
49
|
-
'web-tree-sitter': 'ABI-locked against tree-sitter-wasms; see code-graph engine pin',
|
|
50
|
-
'tree-sitter-wasms': 'ABI-locked against web-tree-sitter; see code-graph engine pin',
|
|
51
|
-
};
|
|
54
|
+
export const EXACT_PIN_EXCEPTIONS: Readonly<Record<string, string>> = {};
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
57
|
* Floors that are non-settled ON PURPOSE, because every version below them is
|
|
@@ -74,8 +77,15 @@ const EXACT_VERSION = /^\d+\.\d+\.\d+$/;
|
|
|
74
77
|
/**
|
|
75
78
|
* Every violation in `dependencies`, as human-readable lines. Empty array
|
|
76
79
|
* means the manifest cannot produce a freshest-patch `ETARGET` for a consumer.
|
|
80
|
+
*
|
|
81
|
+
* `exactPinExceptions` defaults to the module's list and is injectable so the
|
|
82
|
+
* allow-an-exact-pin branch stays covered now that the real list is empty —
|
|
83
|
+
* otherwise the only way to test it would be to add a fake production entry.
|
|
77
84
|
*/
|
|
78
|
-
export function evaluate(
|
|
85
|
+
export function evaluate(
|
|
86
|
+
dependencies: Readonly<Record<string, string>>,
|
|
87
|
+
exactPinExceptions: Readonly<Record<string, string>> = EXACT_PIN_EXCEPTIONS,
|
|
88
|
+
): string[] {
|
|
79
89
|
const errors: string[] = [];
|
|
80
90
|
for (const [name, range] of Object.entries(dependencies)) {
|
|
81
91
|
const security = SECURITY_FLOOR_EXCEPTIONS[name];
|
|
@@ -89,7 +99,7 @@ export function evaluate(dependencies: Readonly<Record<string, string>>): string
|
|
|
89
99
|
continue;
|
|
90
100
|
}
|
|
91
101
|
if (EXACT_VERSION.test(range)) {
|
|
92
|
-
if (!(name in
|
|
102
|
+
if (!(name in exactPinExceptions)) {
|
|
93
103
|
errors.push(
|
|
94
104
|
`${name}@${range}: exact pin without an entry in EXACT_PIN_EXCEPTIONS — ` +
|
|
95
105
|
'add the reason there, or use a settled caret floor (^X.Y.0)',
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
* Exit codes: 0 all enforced gates cleared their floor · 1 a gate is blind,
|
|
32
32
|
* collapsed, or silent · 2 the manifest itself is missing/empty/malformed.
|
|
33
33
|
*/
|
|
34
|
-
import { spawnSync } from 'node:child_process';
|
|
35
34
|
import * as fs from 'node:fs';
|
|
36
35
|
import * as path from 'node:path';
|
|
37
36
|
import process from 'node:process';
|
|
38
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
39
38
|
|
|
40
39
|
import yaml from 'js-yaml';
|
|
40
|
+
import { runCountedProbe } from './_lib/counted_probe.js';
|
|
41
41
|
|
|
42
42
|
const _HERE = fileURLToPath(import.meta.url);
|
|
43
43
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -184,11 +184,11 @@ export function classify(
|
|
|
184
184
|
|
|
185
185
|
function run_gate(spec: GateSpec): { scanned: number | null; crashed: boolean; exit_code: number | null } {
|
|
186
186
|
const runner = path.join(REPO_ROOT, 'scripts-run');
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
191
|
-
if (r.
|
|
187
|
+
// Bounded read: a gate whose output overflowed would lose its `scanned:`
|
|
188
|
+
// line and be classified `silent` — the scan-scope guard reporting a dead
|
|
189
|
+
// scope that is merely truncated. `runCountedProbe` throws instead.
|
|
190
|
+
const r = runCountedProbe(runner, [`src/scripts/${spec.id}`, ...spec.argv], { cwd: REPO_ROOT });
|
|
191
|
+
if (r.failure !== null && r.status === null) return { scanned: null, crashed: true, exit_code: null };
|
|
192
192
|
// A gate may legitimately exit non-zero (it found real violations) and still
|
|
193
193
|
// have scanned plenty — coverage and verdict are different questions.
|
|
194
194
|
return { scanned: parse_scanned(`${r.stdout}\n${r.stderr}`), crashed: false, exit_code: r.status };
|
|
@@ -36,6 +36,7 @@ import * as fs from 'node:fs';
|
|
|
36
36
|
import * as path from 'node:path';
|
|
37
37
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
38
38
|
import { KERNEL_RULE_FILENAMES } from './_lib/kernel_rules.js';
|
|
39
|
+
import { runCountedProbe } from './_lib/counted_probe.js';
|
|
39
40
|
|
|
40
41
|
const KERNEL_RULES: ReadonlySet<string> = KERNEL_RULE_FILENAMES;
|
|
41
42
|
|
|
@@ -56,13 +57,13 @@ const KERNEL_DIR = 'src/rules';
|
|
|
56
57
|
const DEFAULT_LABEL = 'bundled-always-rules-acknowledged';
|
|
57
58
|
|
|
58
59
|
function _git_changed_files(base_ref: string): string[] {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
});
|
|
60
|
+
// Bounded read: a truncated diff would shrink the change set and let the
|
|
61
|
+
// bundle guard pass on files it never saw.
|
|
62
|
+
const res = runCountedProbe('git', ['diff', '--name-only', `${base_ref}...HEAD`]);
|
|
62
63
|
// Python uses check_output(stderr=STDOUT, text=True): on non-zero it raises
|
|
63
64
|
// CalledProcessError and the handler prints the combined output. spawnSync
|
|
64
65
|
// does not raise; mirror by checking status and combining stdout+stderr.
|
|
65
|
-
if (res.
|
|
66
|
+
if (!res.ok) {
|
|
66
67
|
const combined = `${res.stdout ?? ''}${res.stderr ?? ''}`;
|
|
67
68
|
process.stderr.write(`❌ git diff failed: ${combined.trim()}\n`);
|
|
68
69
|
return [];
|
|
@@ -113,20 +113,6 @@ export function router_triggers(router: Json): Map<string, Array<Record<string,
|
|
|
113
113
|
return map;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
// trigger_coverage.ts word semantics: lowercase alpha-led tokens, len > 2.
|
|
117
|
-
const _WORD = /[a-z][a-z0-9_]+/g;
|
|
118
|
-
|
|
119
|
-
function _tokens(text: string): Set<string> {
|
|
120
|
-
const out = new Set<string>();
|
|
121
|
-
const low = text.toLowerCase();
|
|
122
|
-
_WORD.lastIndex = 0;
|
|
123
|
-
let m: RegExpExecArray | null;
|
|
124
|
-
while ((m = _WORD.exec(low)) !== null) {
|
|
125
|
-
if (m[0].length > 2) out.add(m[0]);
|
|
126
|
-
}
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
116
|
/**
|
|
131
117
|
* Whether a task exercises ≥1 trigger of the rule (prompt↔trigger
|
|
132
118
|
* falsifiability, Phase 3). Kernel rules always fire.
|
|
@@ -140,16 +126,12 @@ export function task_fires_rule(
|
|
|
140
126
|
if (kernel.has(ruleId)) return true;
|
|
141
127
|
const triggers = router_triggers(router).get(ruleId) ?? [];
|
|
142
128
|
const low = task.prompt.toLowerCase();
|
|
143
|
-
const toks = _tokens(task.prompt);
|
|
144
129
|
const files = Array.isArray(task.context_files) ? task.context_files : [];
|
|
145
130
|
const cmd = typeof task.command === 'string' ? task.command : '';
|
|
146
131
|
for (const trig of triggers) {
|
|
147
132
|
if ('keyword' in trig || 'phrase' in trig) {
|
|
148
133
|
const needle = String(trig.keyword ?? trig.phrase).toLowerCase();
|
|
149
134
|
if (needle && low.includes(needle)) return true;
|
|
150
|
-
} else if ('intent' in trig) {
|
|
151
|
-
const words = _tokens(String(trig.intent));
|
|
152
|
-
if (words.size > 0 && [...words].every((w) => toks.has(w))) return true;
|
|
153
135
|
} else if ('path_prefix' in trig) {
|
|
154
136
|
const prefix = String(trig.path_prefix);
|
|
155
137
|
if (files.some((f) => f.startsWith(prefix))) return true;
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* WASM tree-sitter loader for the code-graph engine.
|
|
3
3
|
*
|
|
4
4
|
* Pinned pair (ABI-verified 2026-07-23): `web-tree-sitter@0.24.7` +
|
|
5
|
-
* `tree-sitter-wasms@0.1.13` → grammar ABI 14. The ABI smoke test
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* no node-gyp — grammars load from `node_modules` via the resolved
|
|
5
|
+
* `tree-sitter-wasms@0.1.13` → grammar ABI 14. The ABI smoke test lives in
|
|
6
|
+
* `tests/scripts/code_graph.test.ts`; it runs in this repo because the pair is
|
|
7
|
+
* a devDependency. Consumers install neither (see `INSTALL_HINT` below). No
|
|
8
|
+
* network, no node-gyp — grammars load from `node_modules` via the resolved
|
|
9
|
+
* path.
|
|
9
10
|
*
|
|
10
11
|
* web-tree-sitter 0.24.x is CJS with a single default export (the Parser
|
|
11
12
|
* class) and the classic `Parser.Language.load()` API; we reach it through
|
|
@@ -51,10 +52,32 @@ interface ParserStatic {
|
|
|
51
52
|
Language: { load(wasmPath: string): Promise<TsLanguage> };
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The parser pair is a devDependency, not a runtime one: this repo's tests still
|
|
57
|
+
* exercise the loader, while a consumer install resolves neither. The engine is
|
|
58
|
+
* permanently `enabled: false` (measured recall 0.365 vs grep 0.797), so
|
|
59
|
+
* shipping ~51 MB of WASM to every consumer for a path none of them can reach
|
|
60
|
+
* was pure install cost. A consumer re-enabling the engine installs the
|
|
61
|
+
* ABI-locked pair themselves — this message carries the pin, because
|
|
62
|
+
* `check_dependency_floors` only scans the `dependencies` block.
|
|
63
|
+
*/
|
|
64
|
+
const INSTALL_HINT =
|
|
65
|
+
'the code-graph engine needs its parser pair, which this package no longer ships to consumers ' +
|
|
66
|
+
'(the engine is disabled by a recorded null result). Install the ABI-locked pair:\n' +
|
|
67
|
+
' npm i web-tree-sitter@0.24.7 tree-sitter-wasms@0.1.13\n' +
|
|
68
|
+
'Both versions are exact on purpose — the pair is ABI-coupled (grammar ABI 14) ' +
|
|
69
|
+
'and must move together.';
|
|
70
|
+
|
|
54
71
|
let _wasmDir: string | null = null;
|
|
55
72
|
function wasmDir(): string {
|
|
56
73
|
if (_wasmDir === null) {
|
|
57
|
-
|
|
74
|
+
let pkgJson: string;
|
|
75
|
+
try {
|
|
76
|
+
pkgJson = require.resolve('tree-sitter-wasms/package.json');
|
|
77
|
+
} catch {
|
|
78
|
+
throw new Error(`tree-sitter-wasms is not installed — ${INSTALL_HINT}`);
|
|
79
|
+
}
|
|
80
|
+
_wasmDir = path.join(path.dirname(pkgJson), 'out');
|
|
58
81
|
}
|
|
59
82
|
return _wasmDir;
|
|
60
83
|
}
|
|
@@ -72,7 +95,13 @@ function _Parser(): ParserStatic {
|
|
|
72
95
|
// Require exactly once and cache the class reference — repeated
|
|
73
96
|
// createRequire calls under tsx can hand back a module view whose static
|
|
74
97
|
// `Language` is not yet populated, which breaks the second grammar load.
|
|
75
|
-
if (_mod === null)
|
|
98
|
+
if (_mod === null) {
|
|
99
|
+
try {
|
|
100
|
+
_mod = require('web-tree-sitter') as ParserStatic;
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Error(`web-tree-sitter is not installed — ${INSTALL_HINT}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
76
105
|
return _mod;
|
|
77
106
|
}
|
|
78
107
|
|
|
@@ -73,14 +73,9 @@ const LEGACY_TIER_MAP: Record<string, string> = {
|
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
const ALLOWED_TIERS = new Set(['kernel', 'tier-1', 'tier-2']);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'intent',
|
|
80
|
-
'file_pattern',
|
|
81
|
-
'path_prefix',
|
|
82
|
-
'command',
|
|
83
|
-
]);
|
|
76
|
+
// `intent` was removed: it never auto-matched at runtime, so declaring one gave
|
|
77
|
+
// the author a false activation path. Compiling one is now a hard error.
|
|
78
|
+
const ALLOWED_TRIGGER_KEYS = new Set(['keyword', 'phrase', 'file_pattern', 'path_prefix', 'command']);
|
|
84
79
|
|
|
85
80
|
function _parse_frontmatter(text: string): JsonObject {
|
|
86
81
|
if (!text.startsWith('---\n')) {
|
package/src/scripts/condense.ts
CHANGED
|
@@ -1176,7 +1176,7 @@ function _yaml_scalar(value: string): string {
|
|
|
1176
1176
|
* Derive host-native activation globs from a rule's `triggers:` frontmatter
|
|
1177
1177
|
* (road-to-request-scoped-rule-load Phase 2). `file_pattern` triggers map
|
|
1178
1178
|
* verbatim; `path_prefix` triggers map to `<prefix>**`. Keyword / phrase /
|
|
1179
|
-
*
|
|
1179
|
+
* command triggers produce no glob — those rules stay
|
|
1180
1180
|
* description-activated (Cursor Agent-Requested / Windsurf model_decision).
|
|
1181
1181
|
*/
|
|
1182
1182
|
export function derive_trigger_globs(meta: Record<string, unknown>): string[] {
|
|
@@ -1224,7 +1224,7 @@ export function _emit_windsurf_rule(source: string, target: string): void {
|
|
|
1224
1224
|
const description = _strip(String(meta['description'] ?? '').replace(/\n/g, ' '));
|
|
1225
1225
|
const always_apply = Boolean(meta['alwaysApply'] || meta['type'] === 'always');
|
|
1226
1226
|
// Path-shaped triggers activate host-natively via Windsurf's `glob`
|
|
1227
|
-
// trigger; keyword/
|
|
1227
|
+
// trigger; keyword/phrase-only rules keep `model_decision`.
|
|
1228
1228
|
const globs = always_apply ? [] : derive_trigger_globs(meta);
|
|
1229
1229
|
const trigger = always_apply ? 'always_on' : globs.length > 0 ? 'glob' : 'model_decision';
|
|
1230
1230
|
const lines = [
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Emit the scoped-projection skill count — the number `docs/CLAIMS.md`
|
|
4
|
+
* publishes for the default install
|
|
5
|
+
* (road-to-reproducible-artefact-counts).
|
|
6
|
+
*
|
|
7
|
+
* The claim used to read "217 of 288 skills … with the counting method
|
|
8
|
+
* pinned in the benchmark doc", while that benchmark doc said 215 of 286.
|
|
9
|
+
* Both numbers were hand-typed, only one of them tracked the catalog, and
|
|
10
|
+
* the gap grew by one on every skill added because no gate could compare
|
|
11
|
+
* them. This script is the single derivation both now come from.
|
|
12
|
+
*
|
|
13
|
+
* It adds no counting mechanism of its own — that would be a second path
|
|
14
|
+
* free to disagree with the first, which is the defect, not the fix:
|
|
15
|
+
*
|
|
16
|
+
* - the file set is `update_counts.iter_skills()`, the exact walk behind
|
|
17
|
+
* the canonical `count('skills')`, so `projected + pruned === total`
|
|
18
|
+
* holds by construction;
|
|
19
|
+
* - the in/out predicate is `_lib/scoped_projection.is_pruned_under_scoped`,
|
|
20
|
+
* the exact function `install.ts` applies when it prunes a real user's
|
|
21
|
+
* tree, so the published number cannot describe a projection the
|
|
22
|
+
* installer does not perform.
|
|
23
|
+
*
|
|
24
|
+
* `runtime.active_packs` is deliberately empty here: the claim is about the
|
|
25
|
+
* DEFAULT for a new install, and a fresh machine has no overlay. An operator
|
|
26
|
+
* who activates extra packs gets more skills than the claim states — the
|
|
27
|
+
* claim is a floor, and `--json` exposes `active_packs` so a reader can see
|
|
28
|
+
* the assumption rather than infer it.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ./scripts-run src/scripts/count_scoped_projection # "217 of 288"
|
|
32
|
+
* ./scripts-run src/scripts/count_scoped_projection --json
|
|
33
|
+
* ./scripts-run src/scripts/count_scoped_projection --projected
|
|
34
|
+
*
|
|
35
|
+
* Exit codes: 0 ok · 1 the pack registry could not be read.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import * as fs from 'node:fs';
|
|
39
|
+
import * as path from 'node:path';
|
|
40
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
scoped_projection_stats,
|
|
44
|
+
type ScopedProjectionStats,
|
|
45
|
+
} from './_lib/scoped_projection.js';
|
|
46
|
+
import { iter_skills, REPO_ROOT } from './update_counts.js';
|
|
47
|
+
|
|
48
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
49
|
+
|
|
50
|
+
export type { ScopedProjectionStats };
|
|
51
|
+
|
|
52
|
+
/** Partition the canonical skill catalog with the installer's own predicate. */
|
|
53
|
+
export function stats(package_root: string = REPO_ROOT): ScopedProjectionStats {
|
|
54
|
+
return scoped_projection_stats(package_root, iter_skills());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
58
|
+
let s: ScopedProjectionStats;
|
|
59
|
+
try {
|
|
60
|
+
s = stats();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
process.stderr.write(
|
|
63
|
+
`❌ scoped-projection count unavailable: ${(err as Error).message}\n` +
|
|
64
|
+
' (src/config/discovery/packs.yml must exist and parse to a list)\n',
|
|
65
|
+
);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (argv.includes('--json')) {
|
|
70
|
+
process.stdout.write(`${JSON.stringify(s, null, 2)}\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
if (argv.includes('--projected')) {
|
|
74
|
+
process.stdout.write(`${s.projected}\n`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (argv.includes('--total')) {
|
|
78
|
+
process.stdout.write(`${s.total}\n`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write(`${s.projected} of ${s.total}\n`);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _isCliEntry(): boolean {
|
|
86
|
+
if (process.argv[1] === undefined) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
90
|
+
if (import.meta.url === argvUrl) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
95
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
96
|
+
return here === argv;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
103
|
+
process.exit(main(process.argv.slice(2)));
|
|
104
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* regenerated on every `task sync` and must never be hand-edited.
|
|
18
18
|
*
|
|
19
19
|
* For each pack home we emit:
|
|
20
|
-
* - `<home>/pack.yaml` — id, label, owner, requires,
|
|
20
|
+
* - `<home>/pack.yaml` — id, label, owner, requires, artefact_count, …
|
|
21
21
|
* - `<home>/README.md` — pack description + per-artefact description table
|
|
22
22
|
*
|
|
23
23
|
* CLI:
|
|
@@ -40,7 +40,6 @@ const PACKAGES = path.join(ROOT, 'packages');
|
|
|
40
40
|
const SRC_DOMAINS = path.join(ROOT, 'src', 'domains');
|
|
41
41
|
const SRC_PACKS = path.join(ROOT, 'src', 'packs');
|
|
42
42
|
const PACKS_VOCAB = path.join(ROOT, 'src', 'config', 'discovery', 'packs.yml');
|
|
43
|
-
const PACKAGE_JSON = path.join(ROOT, 'package.json');
|
|
44
43
|
const GENERATED_HEADER =
|
|
45
44
|
'# Generated by scripts/generate_pack_manifests.ts — DO NOT EDIT BY HAND\n';
|
|
46
45
|
|
|
@@ -138,14 +137,6 @@ function _vocab_lookup(packsVocab: Dict[]): Record<string, Dict> {
|
|
|
138
137
|
return out;
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
function _package_version(): string {
|
|
142
|
-
if (!_exists(PACKAGE_JSON)) {
|
|
143
|
-
return '0.0.0';
|
|
144
|
-
}
|
|
145
|
-
const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf-8')) as Dict;
|
|
146
|
-
return _pyTruthy(pkg.version) ? String(pkg.version) : '0.0.0';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
140
|
function _artefact_record(p: string, logicalRel: string, fm: Dict): Artefact {
|
|
150
141
|
const name = _pyTruthy(fm.name)
|
|
151
142
|
? String(fm.name)
|
|
@@ -256,12 +247,7 @@ function _category_from_logical(logicalRel: string): string {
|
|
|
256
247
|
* Build the pack.yaml meta dict. Mirrors `_build_pack_yaml`. Insertion
|
|
257
248
|
* order here is irrelevant — `_py_safe_dump` sorts keys (sort_keys=True).
|
|
258
249
|
*/
|
|
259
|
-
function _build_pack_yaml(
|
|
260
|
-
pid: string,
|
|
261
|
-
vocab: Record<string, Dict>,
|
|
262
|
-
artefacts: Artefact[],
|
|
263
|
-
version: string,
|
|
264
|
-
): Dict {
|
|
250
|
+
function _build_pack_yaml(pid: string, vocab: Record<string, Dict>, artefacts: Artefact[]): Dict {
|
|
265
251
|
const meta = vocab[pid] ?? {};
|
|
266
252
|
const label = _pyTruthy(meta.label)
|
|
267
253
|
? String(meta.label)
|
|
@@ -289,7 +275,6 @@ function _build_pack_yaml(
|
|
|
289
275
|
trust_level_default: _pyTruthy(meta.trust_level_default)
|
|
290
276
|
? (meta.trust_level_default as unknown)
|
|
291
277
|
: 'core',
|
|
292
|
-
version,
|
|
293
278
|
artefact_count: artefacts.length,
|
|
294
279
|
};
|
|
295
280
|
if (_isPlainObject(meta.onboarding)) {
|
|
@@ -330,7 +315,6 @@ function _render_readme(packMeta: Dict, artefacts: Artefact[]): string {
|
|
|
330
315
|
const requires = (packMeta.requires as unknown[]) ?? [];
|
|
331
316
|
lines.push(
|
|
332
317
|
`- **id**: \`${String(packMeta.id)}\``,
|
|
333
|
-
`- **version**: \`${String(packMeta.version)}\``,
|
|
334
318
|
`- **owner**: ${owner.map(String).join(', ') || '—'}`,
|
|
335
319
|
`- **requires**: ${requires.map(String).join(', ') || '—'}`,
|
|
336
320
|
`- **artefacts**: ${String(packMeta.artefact_count)}`,
|
|
@@ -369,7 +353,6 @@ function _write_if_changed(p: string, content: string, check: boolean): boolean
|
|
|
369
353
|
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
370
354
|
const check = argv.includes('--check');
|
|
371
355
|
const vocab = _vocab_lookup((_load_yaml(PACKS_VOCAB) as Dict[]) ?? []);
|
|
372
|
-
const version = _package_version();
|
|
373
356
|
const homes = _pack_homes();
|
|
374
357
|
if (homes.length === 0) {
|
|
375
358
|
process.stderr.write(
|
|
@@ -380,7 +363,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
380
363
|
let drift = 0;
|
|
381
364
|
for (const [pid, homeDir, mode] of homes) {
|
|
382
365
|
const artefacts = _collect_artefacts(pid, homeDir, mode);
|
|
383
|
-
const meta = _build_pack_yaml(pid, vocab, artefacts
|
|
366
|
+
const meta = _build_pack_yaml(pid, vocab, artefacts);
|
|
384
367
|
const yamlBody = GENERATED_HEADER + _py_safe_dump(meta);
|
|
385
368
|
const readmeBody = _render_readme(meta, artefacts);
|
|
386
369
|
const label = _relativeToPosix(homeDir, ROOT);
|
|
@@ -40,6 +40,7 @@ import * as path from 'node:path';
|
|
|
40
40
|
|
|
41
41
|
import { is_kernel_rule } from '../_lib/kernel_rules.js';
|
|
42
42
|
import { EDIT_TOOLS } from '../minimal_safe_diff_hook.js';
|
|
43
|
+
import { COMMAND_TOOLS } from '../verify_before_complete_hook.js';
|
|
43
44
|
import { readHookStdin } from './hook_stdin.js';
|
|
44
45
|
|
|
45
46
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -122,25 +123,159 @@ export function targets_kernel_rule(filePath: string): string | null {
|
|
|
122
123
|
return basename.replace(/\.md$/, '');
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
/**
|
|
126
|
+
/** In-place mutators that name their target as a plain argument. */
|
|
127
|
+
const _MUTATOR_VERBS: ReadonlySet<string> = new Set([
|
|
128
|
+
'sed',
|
|
129
|
+
'tee',
|
|
130
|
+
'truncate',
|
|
131
|
+
'rm',
|
|
132
|
+
'shred',
|
|
133
|
+
]);
|
|
134
|
+
/** Verbs whose LAST positional argument is the destination. */
|
|
135
|
+
const _DEST_LAST_VERBS: ReadonlySet<string> = new Set(['mv', 'cp', 'install', 'rsync']);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Shell tokens that, in a command naming a kernel rule path, mean the path is
|
|
139
|
+
* being WRITTEN rather than read. Reads (`cat`, `grep`, `head`, `diff`,
|
|
140
|
+
* `git show`) carry none of these and stay allowed — a kernel rule is
|
|
141
|
+
* immutable, not secret.
|
|
142
|
+
*/
|
|
143
|
+
function _bash_targets_kernel_rule(command: string): string | null {
|
|
144
|
+
// Cheap reject first: no kernel-rule path in the string at all.
|
|
145
|
+
const tokens = command.split(/\s+/).filter((t) => t.length > 0);
|
|
146
|
+
const stripped = tokens.map((t) => t.replace(/^['"]|['"]$/g, ''));
|
|
147
|
+
const ruleOf = (t: string): string | null => targets_kernel_rule(t);
|
|
148
|
+
if (!stripped.some((t) => ruleOf(t) !== null)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 1. Redirection into the path: `> p`, `>> p`, `>p`, `>>p`.
|
|
153
|
+
for (let i = 0; i < stripped.length; i += 1) {
|
|
154
|
+
const tok = stripped[i] as string;
|
|
155
|
+
const inline = /^>{1,2}(.+)$/.exec(tok);
|
|
156
|
+
if (inline) {
|
|
157
|
+
const r = ruleOf(inline[1] as string);
|
|
158
|
+
if (r !== null) {
|
|
159
|
+
return r;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (tok === '>' || tok === '>>') {
|
|
163
|
+
const next = stripped[i + 1];
|
|
164
|
+
if (typeof next === 'string') {
|
|
165
|
+
const r = ruleOf(next);
|
|
166
|
+
if (r !== null) {
|
|
167
|
+
return r;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. A mutator verb anywhere in the pipeline, with the path as an argument.
|
|
174
|
+
// `sed` only counts in its in-place form — `sed 's/x/y/' file` prints.
|
|
175
|
+
const verbs = new Set<string>();
|
|
176
|
+
let prevWasSeparator = true;
|
|
177
|
+
for (const tok of stripped) {
|
|
178
|
+
if (['&&', '||', ';', '|'].includes(tok)) {
|
|
179
|
+
prevWasSeparator = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (prevWasSeparator && !tok.startsWith('-')) {
|
|
183
|
+
verbs.add(path.basename(tok));
|
|
184
|
+
prevWasSeparator = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const sedInPlace = verbs.has('sed') && stripped.some((t) => /^-i/.test(t));
|
|
188
|
+
for (const verb of verbs) {
|
|
189
|
+
if (verb === 'sed' && !sedInPlace) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (_MUTATOR_VERBS.has(verb)) {
|
|
193
|
+
const hit = stripped.map(ruleOf).find((r) => r !== null);
|
|
194
|
+
if (hit !== undefined && hit !== null) {
|
|
195
|
+
return hit;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (_DEST_LAST_VERBS.has(verb)) {
|
|
199
|
+
const last = stripped[stripped.length - 1] as string;
|
|
200
|
+
const r = ruleOf(last);
|
|
201
|
+
if (r !== null) {
|
|
202
|
+
return r;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Return (blocked, reason) for one PreToolUse envelope. Pure — no I/O beyond
|
|
211
|
+
* the arg.
|
|
212
|
+
*
|
|
213
|
+
* Two surfaces, one effect. The Write/Edit branch is the original Layer-1
|
|
214
|
+
* guard. The Bash branch was added by road-to-governance-invariants Phase 1
|
|
215
|
+
* after the S0.2 spike measured the gap: this gate keyed on the TOOL NAME, so
|
|
216
|
+
* `Bash sed -i … src/rules/commit-policy.md` reached the immutable-rule
|
|
217
|
+
* outcome the Write branch refuses, and a two-step
|
|
218
|
+
* `Write docs/staging/<kernel>.md` → `Bash mv … src/rules/<kernel>.md`
|
|
219
|
+
* sequence did the same with every step individually allowed.
|
|
220
|
+
*
|
|
221
|
+
* The Bash branch is deliberately narrow (2026-08-02 council cut, option ii):
|
|
222
|
+
* only redirection into the path, an in-place `sed`, a `tee`/`truncate`/`rm`
|
|
223
|
+
* naming it, or a `mv`/`cp` whose DESTINATION it is. Reads stay allowed — a
|
|
224
|
+
* kernel rule is immutable, not secret — and no attempt is made to understand
|
|
225
|
+
* arbitrary shell. Recognising every conceivable write verb would make this a
|
|
226
|
+
* shell sandbox, which is the failure mode the council named.
|
|
227
|
+
*/
|
|
126
228
|
export function check_envelope(envelope: JsonObject): [boolean, string] {
|
|
127
229
|
const { tool, paths } = _extract(envelope);
|
|
128
|
-
if (!tool
|
|
230
|
+
if (!tool) {
|
|
129
231
|
return [false, ''];
|
|
130
232
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
233
|
+
const deny = (ruleName: string): [boolean, string] => [
|
|
234
|
+
true,
|
|
235
|
+
`kernel rule ${ruleName} is immutable — tighten-only via the override ` +
|
|
236
|
+
'exception registry; see docs/contracts/kernel-membership.md',
|
|
237
|
+
];
|
|
238
|
+
if (EDIT_TOOLS.has(tool)) {
|
|
239
|
+
for (const p of paths) {
|
|
240
|
+
const ruleName = targets_kernel_rule(p);
|
|
241
|
+
if (ruleName !== null) {
|
|
242
|
+
return deny(ruleName);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return [false, ''];
|
|
246
|
+
}
|
|
247
|
+
if (COMMAND_TOOLS.has(tool)) {
|
|
248
|
+
const cmd = _extract_command(envelope);
|
|
249
|
+
if (cmd !== null) {
|
|
250
|
+
const ruleName = _bash_targets_kernel_rule(cmd);
|
|
251
|
+
if (ruleName !== null) {
|
|
252
|
+
return deny(ruleName);
|
|
253
|
+
}
|
|
139
254
|
}
|
|
140
255
|
}
|
|
141
256
|
return [false, ''];
|
|
142
257
|
}
|
|
143
258
|
|
|
259
|
+
/** Best-effort read of a shell command off a PreToolUse envelope. */
|
|
260
|
+
function _extract_command(envelope: JsonObject): string | null {
|
|
261
|
+
const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
|
|
262
|
+
const ti = _isObject(payload['tool_input'])
|
|
263
|
+
? payload['tool_input']
|
|
264
|
+
: _isObject(envelope['tool_input'])
|
|
265
|
+
? envelope['tool_input']
|
|
266
|
+
: null;
|
|
267
|
+
for (const src of [ti, payload]) {
|
|
268
|
+
if (src === null) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const v = src['command'];
|
|
272
|
+
if (typeof v === 'string' && v) {
|
|
273
|
+
return v;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
144
279
|
function _asObject(v: JsonValue | undefined): JsonObject | null {
|
|
145
280
|
return _isObject(v) ? v : null;
|
|
146
281
|
}
|