@event4u/agent-config 8.6.0 → 8.7.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 +29 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- 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/install/emit_host_rules_cli.js +185 -0
- package/dist/install/emit_host_rules_cli.js.map +1 -0
- package/dist/install/install.mjs +6 -0
- package/dist/install/plan.js +4 -1
- package/dist/install/plan.js.map +1 -1
- package/dist/install/rule_scope.js +109 -0
- package/dist/install/rule_scope.js.map +1 -0
- package/dist/install/rule_scope_cli.js +77 -0
- package/dist/install/rule_scope_cli.js.map +1 -0
- package/dist/install/wizard-plan.js +14 -1
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +963 -0
- package/dist/scripts/_lib/agent_settings.js.map +1 -0
- package/dist/scripts/_lib/agent_src.js +607 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -0
- package/dist/scripts/_lib/claude_settings_hooks.js +209 -0
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -0
- package/dist/scripts/_lib/model_tier.js +64 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -0
- package/dist/scripts/_lib/script_output.js +140 -0
- package/dist/scripts/_lib/script_output.js.map +1 -0
- package/dist/scripts/_lib/user_global_paths.js +297 -0
- package/dist/scripts/_lib/user_global_paths.js.map +1 -0
- package/dist/scripts/condense.js +2181 -0
- package/dist/scripts/condense.js.map +1 -0
- package/dist/server/routes/install.js +18 -2
- package/dist/server/routes/install.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +4 -2
- package/src/install/emit_host_rules_cli.ts +190 -0
- package/src/install/plan.ts +11 -1
- package/src/install/rule_scope.ts +120 -0
- package/src/install/rule_scope_cli.ts +79 -0
- package/src/install/wizard-plan.ts +22 -1
- package/src/scripts/check_trigger_eval_presence.ts +171 -0
- package/src/scripts/install.sh +118 -4
- package/src/scripts/trigger_eval_grandfather.json +226 -0
- package/src/server/schemas/settings.ts +6 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-time rule scoping — Pipeline B of consumer-scoped rule projection
|
|
3
|
+
* (road-to-request-scoped-rule-load Phase 1b).
|
|
4
|
+
*
|
|
5
|
+
* The projection filter (`rule_in_scope`, condense.ts) covered only the
|
|
6
|
+
* maintainer projection; the consumer install pipeline shipped rules
|
|
7
|
+
* unfiltered (dead `EXCLUDE_RULES` in install.sh; no exclude at all on the
|
|
8
|
+
* global payload). This module is the ONE scoping surface both install
|
|
9
|
+
* paths consume — it deliberately re-uses `rule_in_scope` so the install
|
|
10
|
+
* semantics can never drift from the projection semantics.
|
|
11
|
+
*
|
|
12
|
+
* ## The `source-of-truth.md` decision (Phase 1b Step 2 — recorded here)
|
|
13
|
+
*
|
|
14
|
+
* Before this module, `install.sh` excluded `source-of-truth.md` from
|
|
15
|
+
* PROJECT installs (hardcoded name) while the GLOBAL payload shipped it —
|
|
16
|
+
* the documented contradiction. Decision: the rule is excluded from BOTH
|
|
17
|
+
* consumer paths, always (even under legacy-all). Rationale: its Iron Law
|
|
18
|
+
* ("NEVER EDIT ANY GENERATED PROJECTION … ALWAYS WORK IN src/") describes
|
|
19
|
+
* THIS repository's build layout; in a consumer project there is no `src/`
|
|
20
|
+
* authoring tree and the installed files ARE the working surface — the rule
|
|
21
|
+
* is not merely useless there, it actively forbids legitimate consumer
|
|
22
|
+
* edits. Shipping it globally was the bug; project installs had it right.
|
|
23
|
+
* The compat list below is the pre-flip carrier of that decision; once the
|
|
24
|
+
* Phase-1 human gate flips consumer defaults to scoped, the rule drops out
|
|
25
|
+
* via its maintainer-only workspace tag and this list can retire.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
|
|
31
|
+
import { rule_in_scope } from '../scripts/condense.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Rules excluded from EVERY consumer install path regardless of scope —
|
|
35
|
+
* same treatment on project and global (see module doc for the decision).
|
|
36
|
+
*/
|
|
37
|
+
export const COMPAT_ALWAYS_EXCLUDED: readonly string[] = ['source-of-truth.md'];
|
|
38
|
+
|
|
39
|
+
/** Workspace/pack scope read from consumer settings. `null` axis = unset. */
|
|
40
|
+
export interface RuleScope {
|
|
41
|
+
readonly workspaces: readonly string[] | null;
|
|
42
|
+
readonly packs: readonly string[] | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const LEGACY_ALL: RuleScope = { workspaces: null, packs: null };
|
|
46
|
+
|
|
47
|
+
/** Non-empty string-array extraction; anything else → null (legacy-all). */
|
|
48
|
+
function _list(value: unknown): readonly string[] | null {
|
|
49
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
50
|
+
return value.map((v) => String(v));
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract `projection.rule_workspaces` / `projection.rule_packs` from an
|
|
57
|
+
* already-parsed settings object (same key contract as condense's
|
|
58
|
+
* `_read_projection_list` — absent / empty = legacy-all).
|
|
59
|
+
*/
|
|
60
|
+
export function ruleScopeFromSettings(settings: Record<string, unknown>): RuleScope {
|
|
61
|
+
const proj = settings['projection'];
|
|
62
|
+
if (typeof proj !== 'object' || proj === null || Array.isArray(proj)) {
|
|
63
|
+
return LEGACY_ALL;
|
|
64
|
+
}
|
|
65
|
+
const p = proj as Record<string, unknown>;
|
|
66
|
+
return {
|
|
67
|
+
workspaces: _list(p['rule_workspaces']),
|
|
68
|
+
packs: _list(p['rule_packs']),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether a rule FILE (path into the shipped `dist/agent-src/rules` tree)
|
|
74
|
+
* arrives in a consumer install under `scope`.
|
|
75
|
+
*
|
|
76
|
+
* Delegates the scoping decision to the projection-path predicate
|
|
77
|
+
* (`rule_in_scope` — kernel always ships, untagged axes fail safe) and adds
|
|
78
|
+
* the compat exclusion on top. Non-rule files (non-`.md`) always arrive.
|
|
79
|
+
*/
|
|
80
|
+
export function ruleFileArrives(sourcePath: string, scope: RuleScope): boolean {
|
|
81
|
+
if (!sourcePath.endsWith('.md')) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
const basename = path.basename(sourcePath);
|
|
85
|
+
if (COMPAT_ALWAYS_EXCLUDED.includes(basename)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return rule_in_scope(sourcePath, scope.workspaces, scope.packs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Basenames from `rulesDir` that are EXCLUDED under `scope` — the shape the
|
|
93
|
+
* bash installer consumes (one basename per line via the CLI below).
|
|
94
|
+
*/
|
|
95
|
+
export function excludedRuleBasenames(rulesDir: string, scope: RuleScope): string[] {
|
|
96
|
+
let entries: string[];
|
|
97
|
+
try {
|
|
98
|
+
entries = fs.readdirSync(rulesDir).sort();
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const out: string[] = [];
|
|
103
|
+
for (const name of entries) {
|
|
104
|
+
if (!name.endsWith('.md')) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const full = path.join(rulesDir, name);
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.statSync(full).isFile()) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!ruleFileArrives(full, scope)) {
|
|
116
|
+
out.push(name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CLI twin surface of {@link module:install/rule_scope} for the bash
|
|
4
|
+
* installer (road-to-request-scoped-rule-load Phase 1b).
|
|
5
|
+
*
|
|
6
|
+
* Prints one EXCLUDED rule basename per line for the given rules source
|
|
7
|
+
* dir + consumer settings file. `install.sh` consumes this to replace its
|
|
8
|
+
* dead hardcoded `EXCLUDE_RULES` list; on any failure the caller falls back
|
|
9
|
+
* to the static compat exclusion (fail-safe: over-ship, never under-ship).
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* rule_scope_cli.ts --rules-dir <dist/agent-src/rules> [--settings <file>]
|
|
13
|
+
*
|
|
14
|
+
* Exit codes: 0 ok (list on stdout, possibly empty) · 2 usage error.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import process from 'node:process';
|
|
19
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
|
+
|
|
21
|
+
import { load_agent_settings } from '../scripts/_lib/agent_settings.js';
|
|
22
|
+
|
|
23
|
+
import { excludedRuleBasenames, ruleScopeFromSettings, LEGACY_ALL } from './rule_scope.js';
|
|
24
|
+
|
|
25
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
26
|
+
let rulesDir = '';
|
|
27
|
+
let settingsFile = '';
|
|
28
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
29
|
+
const arg = argv[i];
|
|
30
|
+
if (arg === '--rules-dir') {
|
|
31
|
+
rulesDir = argv[i + 1] ?? '';
|
|
32
|
+
i += 1;
|
|
33
|
+
} else if (arg === '--settings') {
|
|
34
|
+
settingsFile = argv[i + 1] ?? '';
|
|
35
|
+
i += 1;
|
|
36
|
+
} else {
|
|
37
|
+
process.stderr.write(`rule_scope_cli: unknown argument: ${arg}\n`);
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (rulesDir === '') {
|
|
42
|
+
process.stderr.write('usage: rule_scope_cli.ts --rules-dir <dir> [--settings <file>]\n');
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let scope = LEGACY_ALL;
|
|
47
|
+
if (settingsFile !== '' && fs.existsSync(settingsFile)) {
|
|
48
|
+
try {
|
|
49
|
+
const settings = load_agent_settings({ project_path: settingsFile });
|
|
50
|
+
scope = ruleScopeFromSettings(settings as Record<string, unknown>);
|
|
51
|
+
} catch {
|
|
52
|
+
// Unreadable settings → legacy-all (fail-safe: over-ship).
|
|
53
|
+
scope = LEGACY_ALL;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const name of excludedRuleBasenames(path.resolve(rulesDir), scope)) {
|
|
58
|
+
process.stdout.write(`${name}\n`);
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _isCliEntry(): boolean {
|
|
64
|
+
if (process.argv[1] === undefined) return false;
|
|
65
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
66
|
+
if (import.meta.url === argvUrl) return true;
|
|
67
|
+
try {
|
|
68
|
+
return (
|
|
69
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
70
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
71
|
+
);
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (_isCliEntry()) {
|
|
78
|
+
process.exit(main());
|
|
79
|
+
}
|
|
@@ -23,6 +23,7 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
23
23
|
|
|
24
24
|
import type { ConflictPolicy, InstallPlan, InstallTarget } from './types.js';
|
|
25
25
|
import { buildInstallPlan, type PlanSource } from './plan.js';
|
|
26
|
+
import { ruleFileArrives, LEGACY_ALL, type RuleScope } from './rule_scope.js';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Per-tool user-scope anchor directory, mirrored 1:1 from
|
|
@@ -152,6 +153,14 @@ export interface WizardExpandInputs {
|
|
|
152
153
|
readonly packageRoot: string;
|
|
153
154
|
/** Override for the user's home directory (tests only). */
|
|
154
155
|
readonly home?: string;
|
|
156
|
+
/**
|
|
157
|
+
* Workspace/pack scope applied to `rules` sources
|
|
158
|
+
* (road-to-request-scoped-rule-load Phase 1b). Absent = LEGACY_ALL,
|
|
159
|
+
* which still applies the compat exclusion (`source-of-truth.md`) so
|
|
160
|
+
* the global path finally matches the project path — the recorded
|
|
161
|
+
* decision in `rule_scope.ts`.
|
|
162
|
+
*/
|
|
163
|
+
readonly ruleScope?: RuleScope;
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
/**
|
|
@@ -172,6 +181,8 @@ export interface WizardExpandInputs {
|
|
|
172
181
|
*/
|
|
173
182
|
export function expandWizardSources(inputs: WizardExpandInputs): PlanSource[] {
|
|
174
183
|
const { toolIds, packageRoot, home } = inputs;
|
|
184
|
+
const scope = inputs.ruleScope ?? LEGACY_ALL;
|
|
185
|
+
const ruleFilter = (srcFile: string): boolean => ruleFileArrives(srcFile, scope);
|
|
175
186
|
const out: PlanSource[] = [];
|
|
176
187
|
for (const toolId of toolIds) {
|
|
177
188
|
const deploy = GLOBAL_DEPLOY_SOURCES[toolId];
|
|
@@ -181,7 +192,17 @@ export function expandWizardSources(inputs: WizardExpandInputs): PlanSource[] {
|
|
|
181
192
|
for (const [srcRel, destSub] of deploy) {
|
|
182
193
|
const srcDir = join(packageRoot, srcRel);
|
|
183
194
|
const destDir = destSub.length === 0 ? destRoot : join(destRoot, destSub);
|
|
184
|
-
|
|
195
|
+
// Rule sources get the install-time scoping filter — the same
|
|
196
|
+
// predicate the projection path uses (Phase 1b: Pipeline B may
|
|
197
|
+
// never drift from Pipeline A).
|
|
198
|
+
const isRuleSource = srcRel === 'dist/agent-src/rules';
|
|
199
|
+
out.push({
|
|
200
|
+
toolId,
|
|
201
|
+
srcDir,
|
|
202
|
+
destDir,
|
|
203
|
+
kind: 'deployed',
|
|
204
|
+
...(isRuleSource ? { fileFilter: ruleFilter } : {}),
|
|
205
|
+
});
|
|
185
206
|
}
|
|
186
207
|
}
|
|
187
208
|
return out;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Trigger-eval PRESENCE ratchet (road-to-composition-ratchet Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* `check_trigger_evals.ts` validates freshness + structure of triggers.json
|
|
6
|
+
* files that EXIST; it cannot flag a new skill that ships without one, even
|
|
7
|
+
* though the drafting protocol mandates the stub. This gate closes that gap
|
|
8
|
+
* with a shrink-only grandfather allowlist
|
|
9
|
+
* (`src/scripts/trigger_eval_grandfather.json`, frozen 2026-07-08 at 221
|
|
10
|
+
* entries):
|
|
11
|
+
*
|
|
12
|
+
* 1. Every skill under `src/skills/` must either carry
|
|
13
|
+
* `evals/triggers.json` or appear in the allowlist.
|
|
14
|
+
* 2. An allowlist entry whose skill directory no longer exists is stale
|
|
15
|
+
* and must be removed.
|
|
16
|
+
* 3. An allowlist entry whose skill HAS a triggers.json must be removed —
|
|
17
|
+
* the list may only shrink (ratchet), never regrow.
|
|
18
|
+
*
|
|
19
|
+
* Structure + freshness of existing files stay owned by
|
|
20
|
+
* `check_trigger_evals.ts`; this gate checks presence only.
|
|
21
|
+
*
|
|
22
|
+
* Exit codes: 0 ok · 1 violation(s) · 2 usage / unreadable allowlist.
|
|
23
|
+
* `--quiet` suppresses the ✅ summary (errors always print to stderr).
|
|
24
|
+
*/
|
|
25
|
+
import * as fs from 'node:fs';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
30
|
+
const SKILLS_DIR = path.join(REPO_ROOT, 'src', 'skills');
|
|
31
|
+
const ALLOWLIST_PATH = path.join(REPO_ROOT, 'src', 'scripts', 'trigger_eval_grandfather.json');
|
|
32
|
+
|
|
33
|
+
interface Allowlist {
|
|
34
|
+
skills: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function load_allowlist(p: string): Allowlist | null {
|
|
38
|
+
try {
|
|
39
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as unknown;
|
|
40
|
+
if (
|
|
41
|
+
typeof raw !== 'object' ||
|
|
42
|
+
raw === null ||
|
|
43
|
+
!Array.isArray((raw as { skills?: unknown }).skills) ||
|
|
44
|
+
!(raw as { skills: unknown[] }).skills.every((s) => typeof s === 'string')
|
|
45
|
+
) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return { skills: (raw as { skills: string[] }).skills };
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function list_skill_dirs(root: string): string[] {
|
|
55
|
+
return fs
|
|
56
|
+
.readdirSync(root, { withFileTypes: true })
|
|
57
|
+
.filter((e) => e.isDirectory())
|
|
58
|
+
.map((e) => e.name)
|
|
59
|
+
.sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function has_triggers(skill: string): boolean {
|
|
63
|
+
return fs.existsSync(path.join(SKILLS_DIR, skill, 'evals', 'triggers.json'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Findings {
|
|
67
|
+
errors: string[];
|
|
68
|
+
covered: number;
|
|
69
|
+
total: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function check(skills: string[], allowlisted: string[]): Findings {
|
|
73
|
+
const allow = new Set(allowlisted);
|
|
74
|
+
const skillSet = new Set(skills);
|
|
75
|
+
const errors: string[] = [];
|
|
76
|
+
let covered = 0;
|
|
77
|
+
|
|
78
|
+
for (const skill of skills) {
|
|
79
|
+
if (has_triggers(skill)) {
|
|
80
|
+
covered += 1;
|
|
81
|
+
if (allow.has(skill)) {
|
|
82
|
+
errors.push(
|
|
83
|
+
`${skill}: has evals/triggers.json but is still in the grandfather allowlist — remove the entry (shrink-only ratchet)`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
} else if (!allow.has(skill)) {
|
|
87
|
+
errors.push(
|
|
88
|
+
`${skill}: missing evals/triggers.json and not grandfathered — new/changed skills must ship a trigger-eval set (see artifact-drafting-protocol Phase C)`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const entry of allowlisted) {
|
|
94
|
+
if (!skillSet.has(entry)) {
|
|
95
|
+
errors.push(
|
|
96
|
+
`allowlist entry "${entry}" has no matching skill directory — remove the stale entry`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { errors, covered, total: skills.length };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parse_args(argv: string[]): { quiet: boolean } | null {
|
|
105
|
+
let quiet = false;
|
|
106
|
+
for (const a of argv) {
|
|
107
|
+
if (a === '--quiet' || a === '-q') {
|
|
108
|
+
quiet = true;
|
|
109
|
+
} else {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { quiet };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function main(): number {
|
|
117
|
+
const args = parse_args(process.argv.slice(2));
|
|
118
|
+
if (args === null) {
|
|
119
|
+
process.stderr.write('usage: check_trigger_eval_presence [--quiet]\n');
|
|
120
|
+
return 2;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const allowlist = load_allowlist(ALLOWLIST_PATH);
|
|
124
|
+
if (allowlist === null) {
|
|
125
|
+
process.stderr.write(
|
|
126
|
+
`❌ check-trigger-eval-presence: unreadable or malformed allowlist at ${path.relative(REPO_ROOT, ALLOWLIST_PATH)}\n`,
|
|
127
|
+
);
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { errors, covered, total } = check(list_skill_dirs(SKILLS_DIR), allowlist.skills);
|
|
132
|
+
|
|
133
|
+
if (errors.length) {
|
|
134
|
+
process.stderr.write('❌ check-trigger-eval-presence: presence-ratchet violation(s):\n');
|
|
135
|
+
for (const e of errors) {
|
|
136
|
+
process.stderr.write(` - ${e}\n`);
|
|
137
|
+
}
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!args.quiet) {
|
|
142
|
+
process.stdout.write(
|
|
143
|
+
`✅ check-trigger-eval-presence: ${covered}/${total} skills carry evals/triggers.json (${allowlist.skills.length} grandfathered, shrink-only)\n`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
150
|
+
function _isCliEntry(): boolean {
|
|
151
|
+
if (process.argv[1] === undefined) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
155
|
+
if (import.meta.url === argvUrl) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
160
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
161
|
+
return here === argv;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
168
|
+
process.exit(main());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { ALLOWLIST_PATH, check, load_allowlist, parse_args, main };
|
package/src/scripts/install.sh
CHANGED
|
@@ -23,8 +23,14 @@ set -euo pipefail
|
|
|
23
23
|
COPY_DIRS="rules" # Subdirectories where files must be real copies (space-separated)
|
|
24
24
|
|
|
25
25
|
# Rules that are internal to the agent-config package and should NOT be shipped to consumers.
|
|
26
|
-
#
|
|
27
|
-
|
|
26
|
+
# Static COMPAT floor only (see src/install/rule_scope.ts for the recorded
|
|
27
|
+
# source-of-truth.md decision — same treatment on project AND global paths).
|
|
28
|
+
# The full workspace/pack scoping list is resolved at runtime by
|
|
29
|
+
# resolve_excluded_rules() via src/install/rule_scope_cli.ts, which re-uses
|
|
30
|
+
# the projection predicate (rule_in_scope) so install semantics can never
|
|
31
|
+
# drift from projection semantics. This variable is the fail-safe fallback
|
|
32
|
+
# when no node/tsx runtime is available (over-ship, never under-ship).
|
|
33
|
+
EXCLUDE_RULES="source-of-truth.md"
|
|
28
34
|
|
|
29
35
|
# Files inside target/.augment/ that are NOT managed by sync (created by the bridge installer).
|
|
30
36
|
# Never remove them in clean_stale even though they are absent in the source manifest.
|
|
@@ -284,6 +290,47 @@ create_symlink() {
|
|
|
284
290
|
|
|
285
291
|
# --- Core functions ---
|
|
286
292
|
|
|
293
|
+
# Resolve the full excluded-rule list for this install via the shared
|
|
294
|
+
# scoping CLI (workspace/pack scope from <TARGET>/.agent-settings.yml,
|
|
295
|
+
# router/frontmatter truth, kernel-safe). Overwrites EXCLUDE_RULES on
|
|
296
|
+
# success; keeps the static compat fallback on any failure
|
|
297
|
+
# (road-to-request-scoped-rule-load Phase 1b).
|
|
298
|
+
resolve_excluded_rules() {
|
|
299
|
+
local rules_src="$SOURCE_PAYLOAD/rules"
|
|
300
|
+
local settings_file="$TARGET_DIR/.agent-settings.yml"
|
|
301
|
+
local cli="$SOURCE_DIR/src/install/rule_scope_cli.ts"
|
|
302
|
+
[[ -f "$cli" && -d "$rules_src" ]] || { log_verbose "rule-scope CLI unavailable — static exclude fallback"; return 0; }
|
|
303
|
+
|
|
304
|
+
local tsx_bin=""
|
|
305
|
+
if [[ -x "$SOURCE_DIR/node_modules/.bin/tsx" ]]; then
|
|
306
|
+
tsx_bin="$SOURCE_DIR/node_modules/.bin/tsx"
|
|
307
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
308
|
+
tsx_bin="npx --yes tsx"
|
|
309
|
+
else
|
|
310
|
+
log_verbose "no tsx/npx runtime — static exclude fallback"
|
|
311
|
+
return 0
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
local resolved
|
|
315
|
+
local -a cli_args=(--rules-dir "$rules_src")
|
|
316
|
+
[[ -f "$settings_file" ]] && cli_args+=(--settings "$settings_file")
|
|
317
|
+
# TMPDIR pinned for the child: tsx writes its transform cache under
|
|
318
|
+
# $TMPDIR — on macOS TMPDIR is exported, and callers (tests, wrappers)
|
|
319
|
+
# may point it INTO the install target, leaking tsx-cache files into a
|
|
320
|
+
# consumer tree / breaking dry-run's zero-file guarantee.
|
|
321
|
+
# shellcheck disable=SC2086
|
|
322
|
+
if resolved=$(TMPDIR=/tmp $tsx_bin "$cli" "${cli_args[@]}" 2>/dev/null); then
|
|
323
|
+
# Newline list → space list (is_excluded_rule iterates words).
|
|
324
|
+
EXCLUDE_RULES="$(echo "$resolved" | tr '\n' ' ')"
|
|
325
|
+
local n
|
|
326
|
+
n="$(echo "$resolved" | grep -c . || true)"
|
|
327
|
+
log_verbose "rule scope resolved: $n excluded rule(s)"
|
|
328
|
+
else
|
|
329
|
+
log_verbose "rule-scope CLI failed — static exclude fallback"
|
|
330
|
+
fi
|
|
331
|
+
return 0
|
|
332
|
+
}
|
|
333
|
+
|
|
287
334
|
# Check if a relative path matches an excluded rule
|
|
288
335
|
is_excluded_rule() {
|
|
289
336
|
local rel_path="$1"
|
|
@@ -470,6 +517,60 @@ clean_stale() {
|
|
|
470
517
|
}
|
|
471
518
|
|
|
472
519
|
|
|
520
|
+
# Emit host-NATIVE rule surfaces for consumer Cursor (.mdc with globs) and
|
|
521
|
+
# Windsurf (.windsurf/rules + .windsurfrules) via the exported condense
|
|
522
|
+
# emitters — so consumer installs carry the same trigger signal as the
|
|
523
|
+
# maintainer projection (road-to-request-scoped-rule-load Phase 1b Step 3;
|
|
524
|
+
# the recorded install-time-vs-dist decision lives in
|
|
525
|
+
# src/install/emit_host_rules_cli.ts). Sets CURSOR_NATIVE_EMITTED /
|
|
526
|
+
# WINDSURF_NATIVE_EMITTED so the legacy surfaces (raw-md symlink farm,
|
|
527
|
+
# bash frontmatter-stripping concatenator) run only as no-node fallbacks.
|
|
528
|
+
CURSOR_NATIVE_EMITTED=false
|
|
529
|
+
WINDSURF_NATIVE_EMITTED=false
|
|
530
|
+
emit_host_rules() {
|
|
531
|
+
local project_root="$1"
|
|
532
|
+
local rules_dir="$project_root/.augment/rules"
|
|
533
|
+
local cli="$SOURCE_DIR/src/install/emit_host_rules_cli.ts"
|
|
534
|
+
[[ -d "$rules_dir" && -f "$cli" ]] || return 0
|
|
535
|
+
|
|
536
|
+
local -a tools=()
|
|
537
|
+
is_tool_enabled "cursor" && tools+=("cursor")
|
|
538
|
+
is_tool_enabled "windsurf" && tools+=("windsurf")
|
|
539
|
+
[[ ${#tools[@]} -gt 0 ]] || return 0
|
|
540
|
+
|
|
541
|
+
if $DRY_RUN; then
|
|
542
|
+
log_verbose "emit host-native rules (${tools[*]})"
|
|
543
|
+
return 0
|
|
544
|
+
fi
|
|
545
|
+
|
|
546
|
+
local tsx_bin=""
|
|
547
|
+
if [[ -x "$SOURCE_DIR/node_modules/.bin/tsx" ]]; then
|
|
548
|
+
tsx_bin="$SOURCE_DIR/node_modules/.bin/tsx"
|
|
549
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
550
|
+
tsx_bin="npx --yes tsx"
|
|
551
|
+
else
|
|
552
|
+
log_verbose "no tsx/npx runtime — legacy host-rule surfaces"
|
|
553
|
+
return 0
|
|
554
|
+
fi
|
|
555
|
+
|
|
556
|
+
local tools_csv
|
|
557
|
+
tools_csv="$(IFS=,; echo "${tools[*]}")"
|
|
558
|
+
# TMPDIR pinned: tsx cache must never land in the install target.
|
|
559
|
+
# shellcheck disable=SC2086
|
|
560
|
+
if TMPDIR=/tmp $tsx_bin "$cli" --rules-dir "$rules_dir" \
|
|
561
|
+
--project-root "$project_root" --tools "$tools_csv" >/dev/null 2>&1; then
|
|
562
|
+
for tool in "${tools[@]}"; do
|
|
563
|
+
case "$tool" in
|
|
564
|
+
cursor) CURSOR_NATIVE_EMITTED=true; log_info "Emitted native .cursor/rules/*.mdc" ;;
|
|
565
|
+
windsurf) WINDSURF_NATIVE_EMITTED=true; log_info "Emitted native .windsurf/rules + .windsurfrules" ;;
|
|
566
|
+
esac
|
|
567
|
+
done
|
|
568
|
+
else
|
|
569
|
+
log_verbose "host-rule emission failed — legacy fallback surfaces"
|
|
570
|
+
fi
|
|
571
|
+
return 0
|
|
572
|
+
}
|
|
573
|
+
|
|
473
574
|
# Create tool-specific rule symlinks (filtered by --tools selection).
|
|
474
575
|
# Map: tool ID → (target dir, relative prefix from target → .augment/rules).
|
|
475
576
|
create_tool_symlinks() {
|
|
@@ -482,7 +583,11 @@ create_tool_symlinks() {
|
|
|
482
583
|
local -a tool_dirs=()
|
|
483
584
|
local -a rel_prefixes=()
|
|
484
585
|
is_tool_enabled "claude-code" && { tool_ids+=("claude-code"); tool_dirs+=(".claude/rules"); rel_prefixes+=("../../.augment/rules"); }
|
|
485
|
-
|
|
586
|
+
# Cursor gets NATIVE .mdc files when emit_host_rules succeeded — the
|
|
587
|
+
# raw-md symlink farm is the no-node fallback only (Phase 1b Step 3).
|
|
588
|
+
if ! $CURSOR_NATIVE_EMITTED; then
|
|
589
|
+
is_tool_enabled "cursor" && { tool_ids+=("cursor"); tool_dirs+=(".cursor/rules"); rel_prefixes+=("../../.augment/rules"); }
|
|
590
|
+
fi
|
|
486
591
|
is_tool_enabled "cline" && { tool_ids+=("cline"); tool_dirs+=(".clinerules"); rel_prefixes+=("../.augment/rules"); }
|
|
487
592
|
|
|
488
593
|
if [[ ${#tool_dirs[@]} -eq 0 ]]; then
|
|
@@ -596,6 +701,9 @@ generate_windsurfrules() {
|
|
|
596
701
|
local project_root="$1"
|
|
597
702
|
local rules_dir="$project_root/.augment/rules"
|
|
598
703
|
|
|
704
|
+
# Native emission (frontmatter-aware, trigger-preserving) already wrote
|
|
705
|
+
# .windsurfrules — this bash concatenator is the no-node fallback only.
|
|
706
|
+
$WINDSURF_NATIVE_EMITTED && { log_verbose "skip bash .windsurfrules (native emitted)"; return 0; }
|
|
599
707
|
[[ -d "$rules_dir" ]] || return 0
|
|
600
708
|
is_tool_enabled "windsurf" || { log_verbose "skip .windsurfrules (windsurf not selected)"; return 0; }
|
|
601
709
|
|
|
@@ -1107,6 +1215,10 @@ main() {
|
|
|
1107
1215
|
# install the file does not exist yet → defaults preserved.
|
|
1108
1216
|
resolve_settings
|
|
1109
1217
|
|
|
1218
|
+
# 0c. Resolve the excluded-rule set for this install (workspace/pack
|
|
1219
|
+
# scope via the shared rule_scope CLI; static compat fallback).
|
|
1220
|
+
resolve_excluded_rules
|
|
1221
|
+
|
|
1110
1222
|
# 1. Hybrid sync payload → target/.augment/
|
|
1111
1223
|
sync_hybrid "$SOURCE_PAYLOAD" "$TARGET_DIR/.augment"
|
|
1112
1224
|
if $USE_RULES_SYMLINKS; then
|
|
@@ -1130,7 +1242,9 @@ main() {
|
|
|
1130
1242
|
log_verbose "skip .github/copilot-instructions.md (copilot not selected)"
|
|
1131
1243
|
fi
|
|
1132
1244
|
|
|
1133
|
-
# 3.
|
|
1245
|
+
# 3. Emit host-native rule surfaces (cursor .mdc / windsurf), then
|
|
1246
|
+
# create tool-specific symlinks for the remaining tools.
|
|
1247
|
+
emit_host_rules "$TARGET_DIR"
|
|
1134
1248
|
create_tool_symlinks "$TARGET_DIR"
|
|
1135
1249
|
create_skill_symlinks "$TARGET_DIR"
|
|
1136
1250
|
|