@event4u/agent-config 8.6.0 → 8.8.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 +81 -0
- package/CONTRIBUTING.md +25 -0
- package/README.md +7 -4
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +28 -16
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +15 -0
- package/dist/agent-src/rules/active-remediation.md +92 -0
- package/dist/agent-src/rules/broken-access-control.md +106 -0
- package/dist/agent-src/rules/communication-through-line.md +42 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +8 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +18 -1
- package/dist/agent-src/rules/prefer-enums-over-literals.md +65 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +82 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +105 -0
- package/dist/agent-src/skills/ai-code-blindspots/evals/triggers.json +52 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +21 -4
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +20 -0
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +104 -0
- package/dist/agent-src/skills/frontend-render-security/evals/triggers.json +52 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +7 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +4 -0
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +100 -0
- package/dist/agent-src/skills/supply-chain-intake/evals/triggers.json +52 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +43 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +24 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +196 -16
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +12 -4
- 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 +11 -2
- 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/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.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 +4 -1
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +30 -2
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +14 -0
- package/docs/contracts/mcp-tool-inventory.md +31 -30
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +6 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +2 -2
- package/docs/decisions/ADR-117-subagents-auto-default-on.md +88 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/experiences/content_creator.md +2 -0
- package/docs/experiences/finance.md +2 -0
- package/docs/experiences/founder.md +2 -0
- package/docs/experiences/ops.md +2 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/memory-access.md +12 -0
- package/docs/proof.md +45 -3
- package/docs/release-runbook.md +138 -0
- package/docs/second-brain-scope.md +73 -0
- package/docs/succession.md +80 -0
- package/package.json +2 -2
- package/src/config/agent-settings.template.yml +29 -8
- 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/orchestration_gate.ts +12 -8
- package/src/scripts/_lib/orchestration_savings.ts +127 -0
- package/src/scripts/build_proof.ts +38 -0
- package/src/scripts/chat_history.ts +79 -7
- package/src/scripts/check_trigger_eval_presence.ts +171 -0
- package/src/scripts/domain_soundness_status.ts +236 -0
- package/src/scripts/generate_index.ts +14 -0
- package/src/scripts/hot_context_hook.ts +47 -2
- package/src/scripts/install.sh +118 -4
- package/src/scripts/lint_behavioural_eval_freshness.ts +140 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +22 -3
- package/src/scripts/mcp_server/tools.ts +159 -1
- package/src/scripts/memory_lookup.ts +184 -6
- package/src/scripts/memory_replay.ts +285 -0
- package/src/scripts/orchestration_savings_report.ts +100 -0
- package/src/scripts/schemas/domain-truth.schema.json +94 -0
- package/src/scripts/schemas/evals.schema.json +113 -0
- package/src/scripts/second_brain_score.ts +186 -0
- package/src/scripts/session_memory_index.ts +94 -0
- package/src/scripts/skill_eval_coverage.ts +318 -0
- package/src/scripts/trigger_eval_grandfather.json +226 -0
- package/src/server/schemas/settings.ts +11 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Domain-soundness validation status + ratchet
|
|
4
|
+
* (road-to-domain-soundness Phase 1 + Phase 3).
|
|
5
|
+
*
|
|
6
|
+
* The four non-coding profiles (finance, founder, ops, content_creator) SELL
|
|
7
|
+
* concrete domain value (DCF, runway, RICE, incident command, messaging), but
|
|
8
|
+
* the skills are forged on TS/PHP — "promising, not proven" off those stacks.
|
|
9
|
+
* A disclaimer floor bounds liability, NOT correctness. This tool makes the
|
|
10
|
+
* provenance honest and machine-checkable: it enumerates the default-surface
|
|
11
|
+
* domain skills those profiles ship (from source — each profile's
|
|
12
|
+
* `skills_hint`, never hardcoded) and reports each as:
|
|
13
|
+
*
|
|
14
|
+
* - `validated` — ships `evals/domain-truth.json` (a sourced answer key,
|
|
15
|
+
* authored by domain competence, distinct from behavioural
|
|
16
|
+
* evals.json). Presence is the machine signal; a passing
|
|
17
|
+
* run against it is the maintainer's Phase-3 gate.
|
|
18
|
+
* - `unvalidated` — no domain-truth fixture: general-purpose scaffold, domain
|
|
19
|
+
* correctness not independently validated.
|
|
20
|
+
*
|
|
21
|
+
* A domain skill can pass a format/behavioural eval AND carry a disclaimer AND
|
|
22
|
+
* still embed a wrong domain assumption — this axis is the one that catches it.
|
|
23
|
+
*
|
|
24
|
+
* Modes:
|
|
25
|
+
* (default) human report.
|
|
26
|
+
* --json machine-readable.
|
|
27
|
+
* --check ratchet gate (CI): the count of `validated` skills may not
|
|
28
|
+
* drop below the pinned floor (internal/evals/domain-soundness-floor.json);
|
|
29
|
+
* a missing floor is an all-zero floor (bootstrap-inert). The
|
|
30
|
+
* floor only rises via --write-floor after a fixture lands and
|
|
31
|
+
* passes. This is the enforced-honesty gate: it makes a claimed
|
|
32
|
+
* validation impossible to assert without the fixture on disk.
|
|
33
|
+
* --write-floor pin the current validated count as the floor (maintainer
|
|
34
|
+
* action after authoring + running a domain-truth fixture).
|
|
35
|
+
*
|
|
36
|
+
* Exit codes: 0 ok / 1 ratchet regression / 2 usage error.
|
|
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 { parse as parseYaml } from 'yaml';
|
|
43
|
+
|
|
44
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
45
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
46
|
+
|
|
47
|
+
/** The non-coding profiles whose default surface this tool scopes. */
|
|
48
|
+
export const NON_CODING_PROFILES: readonly string[] = [
|
|
49
|
+
'finance',
|
|
50
|
+
'founder',
|
|
51
|
+
'ops',
|
|
52
|
+
'content_creator',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export interface StatusOptions {
|
|
56
|
+
skillsDir?: string;
|
|
57
|
+
profilesDir?: string;
|
|
58
|
+
floorPath?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SkillStatus {
|
|
62
|
+
skill: string;
|
|
63
|
+
status: 'validated' | 'unvalidated';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SoundnessReport {
|
|
67
|
+
validationSet: SkillStatus[];
|
|
68
|
+
validated: number;
|
|
69
|
+
unvalidated: number;
|
|
70
|
+
total: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _skillsDir(o: StatusOptions): string {
|
|
74
|
+
return o.skillsDir ?? path.join(REPO_ROOT, 'src', 'skills');
|
|
75
|
+
}
|
|
76
|
+
function _profilesDir(o: StatusOptions): string {
|
|
77
|
+
return o.profilesDir ?? path.join(REPO_ROOT, 'src', 'agent-src', 'profiles');
|
|
78
|
+
}
|
|
79
|
+
export function _floorPath(o: StatusOptions = {}): string {
|
|
80
|
+
return (
|
|
81
|
+
o.floorPath ??
|
|
82
|
+
path.join(REPO_ROOT, 'internal', 'evals', 'domain-soundness-floor.json')
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Union of the non-coding profiles' `skills_hint`, restricted to real skills. */
|
|
87
|
+
export function validationSet(o: StatusOptions = {}): string[] {
|
|
88
|
+
const profilesDir = _profilesDir(o);
|
|
89
|
+
const skillsDir = _skillsDir(o);
|
|
90
|
+
const out = new Set<string>();
|
|
91
|
+
for (const p of NON_CODING_PROFILES) {
|
|
92
|
+
const f = path.join(profilesDir, `${p}.yml`);
|
|
93
|
+
let doc: unknown;
|
|
94
|
+
try {
|
|
95
|
+
doc = parseYaml(fs.readFileSync(f, 'utf-8'));
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const profile = (doc as { profile?: unknown } | null)?.profile;
|
|
100
|
+
const defaults = (profile as { defaults?: unknown } | null)?.defaults;
|
|
101
|
+
const hint = (defaults as { skills_hint?: unknown } | null)?.skills_hint;
|
|
102
|
+
if (Array.isArray(hint)) {
|
|
103
|
+
for (const s of hint) {
|
|
104
|
+
if (typeof s === 'string' && fs.existsSync(path.join(skillsDir, s, 'SKILL.md'))) {
|
|
105
|
+
out.add(s);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...out].sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _isValidated(skillsDir: string, skill: string): boolean {
|
|
114
|
+
return fs.existsSync(path.join(skillsDir, skill, 'evals', 'domain-truth.json'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function computeStatus(o: StatusOptions = {}): SoundnessReport {
|
|
118
|
+
const skillsDir = _skillsDir(o);
|
|
119
|
+
const set = validationSet(o);
|
|
120
|
+
const validationSetStatus: SkillStatus[] = set.map((skill) => ({
|
|
121
|
+
skill,
|
|
122
|
+
status: _isValidated(skillsDir, skill) ? 'validated' : 'unvalidated',
|
|
123
|
+
}));
|
|
124
|
+
const validated = validationSetStatus.filter((s) => s.status === 'validated').length;
|
|
125
|
+
return {
|
|
126
|
+
validationSet: validationSetStatus,
|
|
127
|
+
validated,
|
|
128
|
+
unvalidated: validationSetStatus.length - validated,
|
|
129
|
+
total: validationSetStatus.length,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _readFloor(floorPath: string): number {
|
|
134
|
+
try {
|
|
135
|
+
const raw = JSON.parse(fs.readFileSync(floorPath, 'utf-8')) as { validated?: number };
|
|
136
|
+
return raw.validated ?? 0;
|
|
137
|
+
} catch {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function writeFloor(o: StatusOptions = {}): SoundnessReport {
|
|
143
|
+
const report = computeStatus(o);
|
|
144
|
+
const p = _floorPath(o);
|
|
145
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
p,
|
|
148
|
+
JSON.stringify(
|
|
149
|
+
{
|
|
150
|
+
_comment:
|
|
151
|
+
'Domain-soundness ratchet floor (road-to-domain-soundness Phase 3). ' +
|
|
152
|
+
'domain_soundness_status --check fails if the validated count drops below this. ' +
|
|
153
|
+
'Regenerate with --write-floor ONLY after a domain-truth fixture lands and passes.',
|
|
154
|
+
validated: report.validated,
|
|
155
|
+
},
|
|
156
|
+
null,
|
|
157
|
+
2,
|
|
158
|
+
) + '\n',
|
|
159
|
+
);
|
|
160
|
+
return report;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function checkRatchet(o: StatusOptions = {}): { ok: boolean; message: string } {
|
|
164
|
+
const report = computeStatus(o);
|
|
165
|
+
const floor = _readFloor(_floorPath(o));
|
|
166
|
+
if (report.validated < floor) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
message: `validated domain skills ${report.validated} < floor ${floor}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { ok: true, message: `validated ${report.validated}/${report.total} (floor ${floor})` };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _renderHuman(r: SoundnessReport): string {
|
|
176
|
+
const lines = [
|
|
177
|
+
'Domain-soundness status — non-coding default-surface skills',
|
|
178
|
+
` validated: ${r.validated}/${r.total}`,
|
|
179
|
+
` unvalidated: ${r.unvalidated}/${r.total}`,
|
|
180
|
+
'',
|
|
181
|
+
];
|
|
182
|
+
for (const s of r.validationSet) {
|
|
183
|
+
const mark = s.status === 'validated' ? '✅' : '·';
|
|
184
|
+
lines.push(` ${mark} ${s.skill.padEnd(28)} ${s.status}`);
|
|
185
|
+
}
|
|
186
|
+
return lines.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
190
|
+
const known = new Set(['--json', '--check', '--write-floor']);
|
|
191
|
+
for (const a of argv) {
|
|
192
|
+
if (!known.has(a)) {
|
|
193
|
+
process.stderr.write('usage: domain_soundness_status [--json | --check | --write-floor]\n');
|
|
194
|
+
return 2;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (argv.includes('--write-floor')) {
|
|
198
|
+
const r = writeFloor();
|
|
199
|
+
process.stdout.write(`✅ domain-soundness floor pinned: validated ${r.validated}/${r.total}\n`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
if (argv.includes('--check')) {
|
|
203
|
+
const { ok, message } = checkRatchet();
|
|
204
|
+
if (!ok) {
|
|
205
|
+
process.stdout.write(`❌ domain-soundness ratchet regressed: ${message}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
process.stdout.write(`✅ domain-soundness ratchet: ${message}.\n`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
const report = computeStatus();
|
|
212
|
+
if (argv.includes('--json')) {
|
|
213
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
process.stdout.write(_renderHuman(report) + '\n');
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _isCliEntry(): boolean {
|
|
221
|
+
if (process.argv[1] === undefined) return false;
|
|
222
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
223
|
+
if (import.meta.url === argvUrl) return true;
|
|
224
|
+
try {
|
|
225
|
+
return (
|
|
226
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
227
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (_isCliEntry()) {
|
|
235
|
+
process.exit(main());
|
|
236
|
+
}
|
|
@@ -410,6 +410,20 @@ export function _render_catalog(
|
|
|
410
410
|
'',
|
|
411
411
|
`## Skills (${skills.length})`,
|
|
412
412
|
'',
|
|
413
|
+
'> **Behavioural-eval coverage is measured, not implied.** A skill is',
|
|
414
|
+
'> behaviourally evaluated only if it ships `evals/evals.json`; most do',
|
|
415
|
+
'> not yet. Coverage is reported per tier (`./scripts-run',
|
|
416
|
+
'> src/scripts/skill_eval_coverage`) and CI-ratcheted so it can only rise.',
|
|
417
|
+
'> Listing here never implies a skill is behaviourally tested — the',
|
|
418
|
+
'> current coverage and its gap are published on [the proof page](proof.md).',
|
|
419
|
+
'',
|
|
420
|
+
'> **Non-coding domain correctness is scoped, not proven.** The',
|
|
421
|
+
'> `finance` / `founder` / `ops` / `content` skills are forged on TS/PHP',
|
|
422
|
+
'> codebases; their embedded domain heuristics are labeled `unvalidated`',
|
|
423
|
+
'> until they pass a sourced `evals/domain-truth.json` fixture',
|
|
424
|
+
'> (`./scripts-run src/scripts/domain_soundness_status`). A disclaimer',
|
|
425
|
+
'> floor bounds liability, not correctness — see [the proof page](proof.md).',
|
|
426
|
+
'',
|
|
413
427
|
_render_table(skills, ['kind', 'name', 'extra', 'description'], '../', _to_shipped_path),
|
|
414
428
|
'',
|
|
415
429
|
`## Rules (${publicRules.length})`,
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import { spawnSync } from 'node:child_process';
|
|
32
32
|
import * as fs from 'node:fs';
|
|
33
|
+
import { createRequire } from 'node:module';
|
|
33
34
|
import * as path from 'node:path';
|
|
34
35
|
import process from 'node:process';
|
|
35
36
|
import { fileURLToPath } from 'node:url';
|
|
@@ -309,6 +310,39 @@ export function restore_hot_context(
|
|
|
309
310
|
return { action: 'inject', reason: `restored (source=${source || 'startup'})`, context: block };
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
// ---------------------------------------------------------------------
|
|
314
|
+
// opt-in memory session index (road-to-memory-retrieval-economy P5)
|
|
315
|
+
// ---------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Build the compact memory index when `memory.session_index: on`; `null`
|
|
319
|
+
* on the default-off path, empty corpus, or any failure (never blocks).
|
|
320
|
+
* Memory roots are cwd-relative (same contract as the MCP tool handlers),
|
|
321
|
+
* so the build runs chdir-wrapped to the workspace root.
|
|
322
|
+
*/
|
|
323
|
+
function _session_index_block_or_null(root: string): string | null {
|
|
324
|
+
try {
|
|
325
|
+
// Lazy require (ESM-safe via createRequire) keeps the default-off
|
|
326
|
+
// fast path free of the memory_lookup + settings-cascade import cost.
|
|
327
|
+
const req = createRequire(import.meta.url);
|
|
328
|
+
const mod = req('./session_memory_index.js') as {
|
|
329
|
+
session_index_enabled: (root: string) => boolean;
|
|
330
|
+
build_session_index_block: () => string | null;
|
|
331
|
+
};
|
|
332
|
+
if (!mod.session_index_enabled(root)) return null;
|
|
333
|
+
const prev = process.cwd();
|
|
334
|
+
try {
|
|
335
|
+
process.chdir(root);
|
|
336
|
+
return mod.build_session_index_block();
|
|
337
|
+
} finally {
|
|
338
|
+
process.chdir(prev);
|
|
339
|
+
}
|
|
340
|
+
} catch (exc) {
|
|
341
|
+
process.stderr.write(`hot-context-hook: session index skipped: ${String(exc)}\n`);
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
312
346
|
// ---------------------------------------------------------------------
|
|
313
347
|
// CLI — dispatcher concern entry point
|
|
314
348
|
// ---------------------------------------------------------------------
|
|
@@ -351,12 +385,23 @@ export function main(): number {
|
|
|
351
385
|
} else if (event === 'session_start') {
|
|
352
386
|
const source = String(payload.source ?? '');
|
|
353
387
|
const decision = restore_hot_context(root, source);
|
|
388
|
+
const blocks: string[] = [];
|
|
354
389
|
if (decision.action === 'inject' && decision.context) {
|
|
390
|
+
blocks.push(decision.context);
|
|
391
|
+
}
|
|
392
|
+
// Opt-in compact memory index (road-to-memory-retrieval-economy
|
|
393
|
+
// P5) — default OFF; rides the same injection surface. Memory
|
|
394
|
+
// roots are cwd-relative, so resolve from the workspace root.
|
|
395
|
+
const indexBlock = _session_index_block_or_null(root);
|
|
396
|
+
if (indexBlock !== null) {
|
|
397
|
+
blocks.push(indexBlock);
|
|
398
|
+
}
|
|
399
|
+
if (blocks.length > 0) {
|
|
355
400
|
process.stdout.write(
|
|
356
401
|
JSON.stringify({
|
|
357
402
|
decision: 'allow',
|
|
358
|
-
reason: decision.reason,
|
|
359
|
-
context:
|
|
403
|
+
reason: decision.action === 'inject' ? decision.reason : 'memory session index',
|
|
404
|
+
context: blocks.join('\n\n'),
|
|
360
405
|
}) + '\n',
|
|
361
406
|
);
|
|
362
407
|
}
|
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
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Lint behavioural-eval freshness — a behavioural `evals.json` that pins the
|
|
4
|
+
* skill body it was authored against must still match the current SKILL.md
|
|
5
|
+
* body, or it is flagged stale (road-to-skill-eval-coverage Phase 3).
|
|
6
|
+
*
|
|
7
|
+
* Sibling to `lint_eval_freshness.ts`, DIFFERENT axis: that gate keeps a
|
|
8
|
+
* corpus-backed skill's TRIGGER eval fresh against an upstream SHA; this gate
|
|
9
|
+
* keeps a skill's BEHAVIOURAL eval fresh against its own SKILL.md body. The
|
|
10
|
+
* two never conflate.
|
|
11
|
+
*
|
|
12
|
+
* A skill's `evals/evals.json` is in scope when it carries a `skill_body_sha`
|
|
13
|
+
* (SHA-256 of the SKILL.md body — post-frontmatter, trimmed — at authoring
|
|
14
|
+
* time). Absent = unpinned, OUT of scope (nothing to keep fresh), mirroring
|
|
15
|
+
* `lint_eval_freshness`'s `upstream: null` handling. So the currently-shipped
|
|
16
|
+
* evals (which carry no pin) never fail — the gate only bites once an author
|
|
17
|
+
* opts in by pinning.
|
|
18
|
+
*
|
|
19
|
+
* In scope, the gate fails when the pinned `skill_body_sha` differs from the
|
|
20
|
+
* current SKILL.md body sha: the body moved, so the eval may no longer
|
|
21
|
+
* exercise it — re-verify and re-pin (`--write-floor`-style human step) rather
|
|
22
|
+
* than let a stale eval pass silently.
|
|
23
|
+
*
|
|
24
|
+
* Deterministic, no token spend: reads on-disk files only. The live re-eval is
|
|
25
|
+
* a separate, spend-bearing step; this gate verifies the pin is current.
|
|
26
|
+
*
|
|
27
|
+
* Exit codes: 0 clean (or none in scope) / 1 at least one stale / 2 usage.
|
|
28
|
+
*/
|
|
29
|
+
import * as crypto from 'node:crypto';
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
35
|
+
export const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
36
|
+
let SKILLS_DIR = path.join(REPO, 'src', 'skills');
|
|
37
|
+
|
|
38
|
+
export function _setSkillsDirForTest(p: string): void {
|
|
39
|
+
SKILLS_DIR = p;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Json = string | number | boolean | null | Json[] | { [k: string]: Json };
|
|
43
|
+
|
|
44
|
+
function _splitBody(text: string): string {
|
|
45
|
+
if (!text.startsWith('---\n')) return text.replace(/\s+$/, '');
|
|
46
|
+
const end = text.indexOf('\n---', 4);
|
|
47
|
+
if (end === -1) return text.replace(/\s+$/, '');
|
|
48
|
+
return text.slice(end + 4).replace(/^\n+/, '').replace(/\s+$/, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** SHA-256 of a skill's SKILL.md body (post-frontmatter, trailing-ws-trimmed). */
|
|
52
|
+
export function skillBodySha(skillMdText: string): string {
|
|
53
|
+
return crypto.createHash('sha256').update(_splitBody(skillMdText), 'utf-8').digest('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FreshnessResult {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
stale: Array<{ skill: string; pinned: string; current: string }>;
|
|
59
|
+
inScope: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function checkFreshness(): FreshnessResult {
|
|
63
|
+
const stale: FreshnessResult['stale'] = [];
|
|
64
|
+
let inScope = 0;
|
|
65
|
+
let entries: fs.Dirent[];
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
68
|
+
} catch {
|
|
69
|
+
return { ok: true, stale, inScope };
|
|
70
|
+
}
|
|
71
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
72
|
+
for (const e of entries) {
|
|
73
|
+
if (!e.isDirectory()) continue;
|
|
74
|
+
const evalsPath = path.join(SKILLS_DIR, e.name, 'evals', 'evals.json');
|
|
75
|
+
const skillMd = path.join(SKILLS_DIR, e.name, 'SKILL.md');
|
|
76
|
+
if (!fs.existsSync(evalsPath) || !fs.existsSync(skillMd)) continue;
|
|
77
|
+
let spec: Json;
|
|
78
|
+
try {
|
|
79
|
+
spec = JSON.parse(fs.readFileSync(evalsPath, 'utf-8')) as Json;
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const pinned =
|
|
84
|
+
spec && typeof spec === 'object' && !Array.isArray(spec)
|
|
85
|
+
? (spec['skill_body_sha'] as string | undefined)
|
|
86
|
+
: undefined;
|
|
87
|
+
if (typeof pinned !== 'string' || pinned === '') continue; // unpinned → out of scope
|
|
88
|
+
inScope += 1;
|
|
89
|
+
const current = skillBodySha(fs.readFileSync(skillMd, 'utf-8'));
|
|
90
|
+
if (current !== pinned) {
|
|
91
|
+
stale.push({ skill: e.name, pinned, current });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { ok: stale.length === 0, stale, inScope };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
98
|
+
let quiet = false;
|
|
99
|
+
for (const a of argv) {
|
|
100
|
+
if (a === '--quiet') quiet = true;
|
|
101
|
+
else {
|
|
102
|
+
process.stderr.write('usage: lint_behavioural_eval_freshness [--quiet]\n');
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const { ok, stale, inScope } = checkFreshness();
|
|
107
|
+
if (!ok) {
|
|
108
|
+
for (const s of stale) {
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
`stale behavioural eval: ${s.skill}/evals/evals.json pinned ${s.pinned.slice(0, 12)}… ` +
|
|
111
|
+
`but SKILL.md body is ${s.current.slice(0, 12)}… — re-verify and re-pin skill_body_sha\n`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
if (!quiet) {
|
|
117
|
+
process.stdout.write(
|
|
118
|
+
`✅ behavioural-eval freshness: ${inScope} pinned eval(s) current (unpinned evals out of scope).\n`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _isCliEntry(): boolean {
|
|
125
|
+
if (process.argv[1] === undefined) return false;
|
|
126
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
127
|
+
if (import.meta.url === argvUrl) return true;
|
|
128
|
+
try {
|
|
129
|
+
return (
|
|
130
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
131
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
132
|
+
);
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (_isCliEntry()) {
|
|
139
|
+
process.exit(main());
|
|
140
|
+
}
|