@event4u/agent-config 8.5.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 +54 -0
- package/dist/agent-src/commands/cost/report.md +20 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +30 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +15 -1
- package/dist/agent-src/contexts/execution/subagent-steering.md +54 -1
- package/dist/agent-src/contexts/model-recommendations.md +19 -0
- package/dist/agent-src/contexts/subagent-configuration.md +8 -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 +4 -4
- 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/docs/CLAIMS.md +7 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +125 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/proof.md +2 -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/_lib/auto_dispatch.ts +63 -0
- package/src/scripts/_lib/subagent_routing.ts +24 -0
- package/src/scripts/_lib/subagent_steering.ts +213 -3
- 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,190 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Consumer host-native rule emission
|
|
4
|
+
* (road-to-request-scoped-rule-load Phase 1b Step 3).
|
|
5
|
+
*
|
|
6
|
+
* Before this CLI, consumer Cursor got a symlink farm to raw `.md` (no
|
|
7
|
+
* `.mdc`, no auto-attach globs) and consumer `.windsurfrules` was built by
|
|
8
|
+
* a bash concatenator that STRIPPED all frontmatter — destroying the
|
|
9
|
+
* host-native trigger signal Phase 2 added to the projection path.
|
|
10
|
+
*
|
|
11
|
+
* ## Recorded decision — install-time emission, not dist artifacts
|
|
12
|
+
*
|
|
13
|
+
* Phase 1b Step 3 asked for one recorded choice: pre-built host artifacts
|
|
14
|
+
* in `dist/` vs emission at install time. Decision: INSTALL-TIME EMISSION,
|
|
15
|
+
* re-using the exported condense emitters (`_emit_cursor_mdc`,
|
|
16
|
+
* `_emit_windsurf_rule`, `strip_frontmatter`) verbatim. Rationale:
|
|
17
|
+
* - a new `dist/agent-src/hosts/` tree would cascade through the
|
|
18
|
+
* discovery manifest, artefact checksums, condensation hashes and the
|
|
19
|
+
* always-budget gates for what is derived data;
|
|
20
|
+
* - dist artifacts are scope-agnostic — install-time emission runs AFTER
|
|
21
|
+
* the Phase-1b rule filter, so scoped installs get scoped host files
|
|
22
|
+
* for free;
|
|
23
|
+
* - re-using the exported emitters keeps consumer output byte-identical
|
|
24
|
+
* to the maintainer projection (Pipeline B never drifts from A).
|
|
25
|
+
* The trade-off (node required at install time) matches the rule-scope
|
|
26
|
+
* resolver: no node → install.sh keeps its legacy fallback surfaces.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* emit_host_rules_cli.ts --rules-dir <installed rules dir>
|
|
30
|
+
* --project-root <consumer root>
|
|
31
|
+
* --tools cursor,windsurf
|
|
32
|
+
*
|
|
33
|
+
* Exit codes: 0 emitted · 2 usage error · 1 emission failure (caller falls
|
|
34
|
+
* back to the legacy surface).
|
|
35
|
+
*/
|
|
36
|
+
import * as fs from 'node:fs';
|
|
37
|
+
import * as path from 'node:path';
|
|
38
|
+
import process from 'node:process';
|
|
39
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
_emit_cursor_mdc,
|
|
43
|
+
_emit_windsurf_rule,
|
|
44
|
+
strip_frontmatter,
|
|
45
|
+
} from '../scripts/condense.js';
|
|
46
|
+
|
|
47
|
+
function _ruleFiles(rulesDir: string): string[] {
|
|
48
|
+
return fs
|
|
49
|
+
.readdirSync(rulesDir)
|
|
50
|
+
.filter((n) => n.endsWith('.md'))
|
|
51
|
+
.sort()
|
|
52
|
+
.map((n) => path.join(rulesDir, n))
|
|
53
|
+
.filter((p) => {
|
|
54
|
+
try {
|
|
55
|
+
return fs.statSync(p).isFile();
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Remove entries not in `valid` (mirror of condense's `_clean_modern_dir`). */
|
|
63
|
+
function _cleanDir(dir: string, valid: ReadonlySet<string>): void {
|
|
64
|
+
let entries: string[];
|
|
65
|
+
try {
|
|
66
|
+
entries = fs.readdirSync(dir);
|
|
67
|
+
} catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const name of entries) {
|
|
71
|
+
if (name === 'README.md' || valid.has(name)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const full = path.join(dir, name);
|
|
75
|
+
try {
|
|
76
|
+
if (fs.lstatSync(full).isDirectory()) {
|
|
77
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
78
|
+
} else {
|
|
79
|
+
fs.unlinkSync(full);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
/* best-effort clean */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function emitCursor(rulesDir: string, projectRoot: string): number {
|
|
88
|
+
const targetDir = path.join(projectRoot, '.cursor', 'rules');
|
|
89
|
+
const files = _ruleFiles(rulesDir);
|
|
90
|
+
const valid = new Set<string>();
|
|
91
|
+
for (const src of files) {
|
|
92
|
+
const name = `${path.basename(src, '.md')}.mdc`;
|
|
93
|
+
_emit_cursor_mdc(src, path.join(targetDir, name));
|
|
94
|
+
valid.add(name);
|
|
95
|
+
}
|
|
96
|
+
_cleanDir(targetDir, valid);
|
|
97
|
+
return files.length;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function emitWindsurf(rulesDir: string, projectRoot: string): number {
|
|
101
|
+
const perRuleDir = path.join(projectRoot, '.windsurf', 'rules');
|
|
102
|
+
const files = _ruleFiles(rulesDir);
|
|
103
|
+
const valid = new Set<string>();
|
|
104
|
+
for (const src of files) {
|
|
105
|
+
const name = path.basename(src);
|
|
106
|
+
_emit_windsurf_rule(src, path.join(perRuleDir, name));
|
|
107
|
+
valid.add(name);
|
|
108
|
+
}
|
|
109
|
+
_cleanDir(perRuleDir, valid);
|
|
110
|
+
|
|
111
|
+
// Concatenated legacy surface — same format as the projection-path
|
|
112
|
+
// generator (condense.generate_windsurfrules), consumer-sourced header.
|
|
113
|
+
const parts = ['# Auto-generated from .augment/rules/ — do not edit directly\n'];
|
|
114
|
+
for (const src of files) {
|
|
115
|
+
const content = strip_frontmatter(fs.readFileSync(src, 'utf-8'));
|
|
116
|
+
parts.push(`---\n\n${content.trim()}\n`);
|
|
117
|
+
}
|
|
118
|
+
fs.writeFileSync(path.join(projectRoot, '.windsurfrules'), parts.join('\n') + '\n', 'utf-8');
|
|
119
|
+
return files.length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
123
|
+
let rulesDir = '';
|
|
124
|
+
let projectRoot = '';
|
|
125
|
+
let tools: string[] = [];
|
|
126
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
127
|
+
const arg = argv[i];
|
|
128
|
+
if (arg === '--rules-dir') {
|
|
129
|
+
rulesDir = argv[i + 1] ?? '';
|
|
130
|
+
i += 1;
|
|
131
|
+
} else if (arg === '--project-root') {
|
|
132
|
+
projectRoot = argv[i + 1] ?? '';
|
|
133
|
+
i += 1;
|
|
134
|
+
} else if (arg === '--tools') {
|
|
135
|
+
tools = (argv[i + 1] ?? '').split(',').filter((t) => t.length > 0);
|
|
136
|
+
i += 1;
|
|
137
|
+
} else {
|
|
138
|
+
process.stderr.write(`emit_host_rules_cli: unknown argument: ${arg}\n`);
|
|
139
|
+
return 2;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (rulesDir === '' || projectRoot === '' || tools.length === 0) {
|
|
143
|
+
process.stderr.write(
|
|
144
|
+
'usage: emit_host_rules_cli.ts --rules-dir <dir> --project-root <dir> --tools cursor,windsurf\n',
|
|
145
|
+
);
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
|
148
|
+
if (!fs.existsSync(rulesDir)) {
|
|
149
|
+
process.stderr.write(`emit_host_rules_cli: rules dir not found: ${rulesDir}\n`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
for (const tool of tools) {
|
|
154
|
+
if (tool === 'cursor') {
|
|
155
|
+
const n = emitCursor(rulesDir, path.resolve(projectRoot));
|
|
156
|
+
process.stdout.write(`cursor: ${n} .mdc rule(s)\n`);
|
|
157
|
+
} else if (tool === 'windsurf') {
|
|
158
|
+
const n = emitWindsurf(rulesDir, path.resolve(projectRoot));
|
|
159
|
+
process.stdout.write(`windsurf: ${n} rule(s) + .windsurfrules\n`);
|
|
160
|
+
} else {
|
|
161
|
+
process.stderr.write(`emit_host_rules_cli: unknown tool: ${tool}\n`);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
} catch (e) {
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
`emit_host_rules_cli: emission failed: ${e instanceof Error ? e.message : String(e)}\n`,
|
|
169
|
+
);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _isCliEntry(): boolean {
|
|
175
|
+
if (process.argv[1] === undefined) return false;
|
|
176
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
177
|
+
if (import.meta.url === argvUrl) return true;
|
|
178
|
+
try {
|
|
179
|
+
return (
|
|
180
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
181
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
182
|
+
);
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (_isCliEntry()) {
|
|
189
|
+
process.exit(main());
|
|
190
|
+
}
|
package/src/install/plan.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface PlanSource {
|
|
|
46
46
|
readonly destDir: string;
|
|
47
47
|
/** Kind recorded on every {@link FileEntry} produced from this source. */
|
|
48
48
|
readonly kind: FileKind;
|
|
49
|
+
/**
|
|
50
|
+
* Optional per-file predicate (absolute source path → include?).
|
|
51
|
+
* Files rejected here never enter the plan — the rule-scoping filter
|
|
52
|
+
* for `rules` sources (road-to-request-scoped-rule-load Phase 1b).
|
|
53
|
+
* Absent = include everything (legacy behaviour).
|
|
54
|
+
*/
|
|
55
|
+
readonly fileFilter?: (srcFile: string) => boolean;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
/** Inputs the planner needs to walk source trees and produce an {@link InstallPlan}. */
|
|
@@ -147,7 +154,10 @@ export function buildInstallPlan(inputs: PlanInputs): InstallPlan {
|
|
|
147
154
|
|
|
148
155
|
for (const source of inputs.sources) {
|
|
149
156
|
const bucket = filesByTool[source.toolId] ?? [];
|
|
150
|
-
|
|
157
|
+
let files = walkSourceTree(source.srcDir);
|
|
158
|
+
if (source.fileFilter !== undefined) {
|
|
159
|
+
files = files.filter(source.fileFilter);
|
|
160
|
+
}
|
|
151
161
|
for (const srcFile of files) {
|
|
152
162
|
const rel = relative(source.srcDir, srcFile);
|
|
153
163
|
const destPath = resolve(source.destDir, rel);
|
|
@@ -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;
|
|
@@ -93,3 +93,66 @@ export function classifyTask(signals: TaskSignals, activation: ActivationInputs)
|
|
|
93
93
|
const action: DispatchAction = activation.auto === 'on' ? 'dispatch' : 'ask';
|
|
94
94
|
return { delegable: true, action, mode, reason };
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
// ── Per-slice tier inference (v1.5 — road-to-cost-aware-model-routing) ──
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Task-TYPE outputs the tier inference keys on. NEVER raw size metrics —
|
|
101
|
+
* size enters only as the negative guard below. Contract:
|
|
102
|
+
* `auto-dispatch-classification.md § Per-slice tier inference`.
|
|
103
|
+
*/
|
|
104
|
+
export type SliceType =
|
|
105
|
+
| 'read-only-fanout' // grep / inventory / discovery targets
|
|
106
|
+
| 'mechanical-covered' // template-driven transform WITH test coverage
|
|
107
|
+
| 'mutating-uncovered' // mutation without test coverage
|
|
108
|
+
| 'synthesis' // review / analysis / judgment slice
|
|
109
|
+
| 'unknown';
|
|
110
|
+
|
|
111
|
+
export interface SliceTierSignals {
|
|
112
|
+
slice_type: SliceType;
|
|
113
|
+
/**
|
|
114
|
+
* Negative size guard: slice scope exceeds the mechanical envelope
|
|
115
|
+
* (multi-file mutation / beyond single responsibility). Revokes a `lite`
|
|
116
|
+
* candidacy — never creates one.
|
|
117
|
+
*/
|
|
118
|
+
exceeds_mechanical_envelope?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type InferredTier = 'lite' | 'medium' | 'inherit';
|
|
122
|
+
|
|
123
|
+
export interface TierInference {
|
|
124
|
+
tier: InferredTier;
|
|
125
|
+
/** 'inferred' for a real downshift decision; 'inherit' when no inference fired. */
|
|
126
|
+
tier_source: 'inferred' | 'inherit';
|
|
127
|
+
reason: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Infer a delegable slice's model tier from its task TYPE. Deterministic,
|
|
132
|
+
* no LLM meta-call. Unknown/ambiguous → inherit (session tier) — never
|
|
133
|
+
* guess down.
|
|
134
|
+
*/
|
|
135
|
+
export function inferSliceTier(signals: SliceTierSignals): TierInference {
|
|
136
|
+
const guard = signals.exceeds_mechanical_envelope === true;
|
|
137
|
+
|
|
138
|
+
switch (signals.slice_type) {
|
|
139
|
+
case 'read-only-fanout':
|
|
140
|
+
case 'mechanical-covered': {
|
|
141
|
+
if (guard) {
|
|
142
|
+
return {
|
|
143
|
+
tier: 'medium',
|
|
144
|
+
tier_source: 'inferred',
|
|
145
|
+
reason: `${signals.slice_type} slice, but scope exceeds the mechanical envelope — lite candidacy revoked`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const cascade = signals.slice_type === 'mechanical-covered' ? ' (verify-fail escalates to medium)' : '';
|
|
149
|
+
return { tier: 'lite', tier_source: 'inferred', reason: `${signals.slice_type} slice${cascade}` };
|
|
150
|
+
}
|
|
151
|
+
case 'mutating-uncovered':
|
|
152
|
+
return { tier: 'medium', tier_source: 'inferred', reason: 'mutating slice without test coverage' };
|
|
153
|
+
case 'synthesis':
|
|
154
|
+
return { tier: 'medium', tier_source: 'inferred', reason: 'synthesis/judgment slice — judge one tier up' };
|
|
155
|
+
default:
|
|
156
|
+
return { tier: 'inherit', tier_source: 'inherit', reason: 'unknown/ambiguous slice type — session tier, never guess down' };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
export type Tier = 'lite' | 'medium' | 'high' | 'inherit';
|
|
19
19
|
export type QuotaPool = 'separate' | 'shared';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Where a resolved tier came from (road-to-cost-aware-model-routing Phase 0):
|
|
23
|
+
* - 'static' — declared by frontmatter / category pin,
|
|
24
|
+
* - 'inferred' — produced by the deterministic per-slice tier inference,
|
|
25
|
+
* - 'inherit' — session tier; no downshift decision was made.
|
|
26
|
+
* Mirrors the `tier_source` telemetry field in
|
|
27
|
+
* `src/agent-src/contexts/execution/orchestration-telemetry.md`.
|
|
28
|
+
*/
|
|
29
|
+
export type TierSource = 'static' | 'inferred' | 'inherit';
|
|
30
|
+
|
|
21
31
|
/** Per-tier model alias overrides (empty string = use the tier's runtime default). */
|
|
22
32
|
export interface ModelMap {
|
|
23
33
|
lite?: string;
|
|
@@ -35,6 +45,12 @@ export interface RoutingInputs {
|
|
|
35
45
|
model_map: ModelMap;
|
|
36
46
|
/** From the host-capability manifest. */
|
|
37
47
|
separate_quota_pool: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* How `task_tier` was produced: 'static' (frontmatter/category pin) or
|
|
50
|
+
* 'inferred' (deterministic per-slice inference). Optional — absent
|
|
51
|
+
* defaults to 'static'. Ignored when the slice runs on the session tier.
|
|
52
|
+
*/
|
|
53
|
+
task_tier_origin?: Exclude<TierSource, 'inherit'>;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
export interface RoutingDecision {
|
|
@@ -44,6 +60,8 @@ export interface RoutingDecision {
|
|
|
44
60
|
model: string;
|
|
45
61
|
/** Whether a separate quota pool is used (bonus) or the shared one. */
|
|
46
62
|
quota_pool: QuotaPool;
|
|
63
|
+
/** Telemetry value for `tier_source`: 'inherit' when no downshift decision applied. */
|
|
64
|
+
tier_source: TierSource;
|
|
47
65
|
reason: string;
|
|
48
66
|
}
|
|
49
67
|
|
|
@@ -64,6 +82,11 @@ export function resolveSubagentRouting(inp: RoutingInputs): RoutingDecision {
|
|
|
64
82
|
const tier: Tier = !inp.downshift || inp.task_tier === 'inherit' ? inp.session_tier : inp.task_tier;
|
|
65
83
|
const downshifted = inp.downshift && inp.task_tier !== 'inherit' && tier !== inp.session_tier;
|
|
66
84
|
|
|
85
|
+
// Telemetry provenance: only a real downshift decision carries its origin;
|
|
86
|
+
// session-tier runs (inherit task, downshift off, or tier == session) are 'inherit'.
|
|
87
|
+
const applied = inp.downshift && inp.task_tier !== 'inherit';
|
|
88
|
+
const tier_source: TierSource = applied ? (inp.task_tier_origin ?? 'static') : 'inherit';
|
|
89
|
+
|
|
67
90
|
// Quota arbitrage: a bonus, only when BOTH the setting and the host allow it.
|
|
68
91
|
const useSeparate = inp.quota_arbitrage && inp.separate_quota_pool;
|
|
69
92
|
const quota_pool: QuotaPool = useSeparate ? 'separate' : 'shared';
|
|
@@ -76,6 +99,7 @@ export function resolveSubagentRouting(inp: RoutingInputs): RoutingDecision {
|
|
|
76
99
|
tier,
|
|
77
100
|
model: modelForTier(tier, inp.model_map),
|
|
78
101
|
quota_pool,
|
|
102
|
+
tier_source,
|
|
79
103
|
reason: reasonParts.join(' · '),
|
|
80
104
|
};
|
|
81
105
|
}
|