@event4u/agent-config 9.10.0 → 9.11.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 +59 -0
- package/README.md +12 -15
- package/agents/templates/.ai-council.yml.example +481 -0
- package/agents/templates/.ai-video.xml.example +247 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +15 -2
- package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +30 -30
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +20 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +20 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +179 -0
- package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +155 -4
- package/dist/agent-src/templates/scripts/work_engine/state.ts +21 -4
- package/dist/cli/initRouting.js +74 -11
- package/dist/cli/initRouting.js.map +1 -1
- package/dist/cli/main.js +14 -3
- package/dist/cli/main.js.map +1 -1
- package/dist/cli-delegate/chunk-4SV3XGFA.js +6910 -0
- package/dist/cli-delegate/chunk-736JCWLM.js +1104 -0
- package/dist/cli-delegate/chunk-7DSAMZFU.js +356 -0
- package/dist/cli-delegate/chunk-7HQ52DHU.js +74 -0
- package/dist/cli-delegate/chunk-BJGXZNPM.js +266 -0
- package/dist/cli-delegate/chunk-FTAISGDQ.js +117 -0
- package/dist/cli-delegate/chunk-GME3Q44L.js +82 -0
- package/dist/cli-delegate/chunk-GRRPEPRN.js +336 -0
- package/dist/cli-delegate/chunk-HJ6DAJHR.js +502 -0
- package/dist/cli-delegate/chunk-IR7P7JWA.js +872 -0
- package/dist/cli-delegate/chunk-J55WI3RR.js +47 -0
- package/dist/cli-delegate/chunk-MCJYCQPF.js +35 -0
- package/dist/cli-delegate/chunk-NDJLNC6Q.js +169 -0
- package/dist/cli-delegate/chunk-OMIVVCG2.js +26 -0
- package/dist/cli-delegate/chunk-OY53J6II.js +7336 -0
- package/dist/cli-delegate/chunk-RDGRHAAB.js +259 -0
- package/dist/cli-delegate/chunk-TFG57JJM.js +1969 -0
- package/dist/cli-delegate/chunk-XX6TOPD7.js +715 -0
- package/dist/cli-delegate/chunk-YMFO6FDN.js +10603 -0
- package/dist/cli-delegate/cmd_analyze_session.js +294 -0
- package/dist/cli-delegate/cmd_conformance.js +646 -0
- package/dist/cli-delegate/cmd_converge.js +326 -0
- package/dist/cli-delegate/cmd_doctor.js +144 -0
- package/dist/cli-delegate/cmd_explain.js +2508 -0
- package/dist/cli-delegate/cmd_export.js +296 -0
- package/dist/cli-delegate/cmd_fleet.js +332 -0
- package/dist/cli-delegate/cmd_memory_get.js +3878 -0
- package/dist/cli-delegate/cmd_migrate.js +11 -0
- package/dist/cli-delegate/cmd_preflight.js +270 -0
- package/dist/cli-delegate/cmd_prune.js +444 -0
- package/dist/cli-delegate/cmd_refresh.js +28 -0
- package/dist/cli-delegate/cmd_settings_check.js +574 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +223 -0
- package/dist/cli-delegate/cmd_sync.js +264 -0
- package/dist/cli-delegate/cmd_uninstall.js +656 -0
- package/dist/cli-delegate/cmd_update.js +425 -0
- package/dist/cli-delegate/cmd_upgrade.js +444 -0
- package/dist/cli-delegate/cmd_validate.js +305 -0
- package/dist/cli-delegate/cmd_versions.js +370 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +14 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +12 -7
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +4 -3
- package/dist/install/emit_host_rules_cli.js +2 -1
- package/dist/install/emit_host_rules_cli.js.map +1 -1
- package/dist/install/install.mjs +410 -378
- package/dist/install/rule_scope.js +5 -1
- package/dist/install/rule_scope.js.map +1 -1
- package/dist/install/toolDetection.js +40 -12
- package/dist/install/toolDetection.js.map +1 -1
- package/dist/install/wizard-plan.js +2 -2
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1614 -765
- package/dist/router.json +1 -1
- package/package.json +4 -2
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +30 -30
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +88 -33
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +20 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +20 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +179 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +155 -4
- package/src/agent-src/templates/scripts/work_engine/state.ts +21 -4
- package/src/config/discovery/packs.yml +7 -0
- package/src/config/evaluator-budgets.json +1 -1
- package/src/config/pack-size-budget.json +27 -0
- package/src/install/emit_host_rules_cli.ts +2 -1
- package/src/install/rule_scope.ts +5 -1
- package/src/install/toolDetection.ts +39 -8
- package/src/install/wizard-plan.ts +2 -2
- package/src/scripts/_cli/cmd_conformance.ts +2 -1
- package/src/scripts/_cli/cmd_converge.ts +2 -1
- package/src/scripts/_cli/cmd_doctor.ts +214 -13
- package/src/scripts/_cli/cmd_export.ts +2 -1
- package/src/scripts/_cli/cmd_fleet.ts +2 -1
- package/src/scripts/_cli/cmd_migrate.ts +2 -1
- package/src/scripts/_cli/cmd_preflight.ts +2 -1
- package/src/scripts/_cli/cmd_refresh.ts +2 -1
- package/src/scripts/_cli/cmd_update.ts +2 -1
- package/src/scripts/_cli/cmd_upgrade.ts +2 -1
- package/src/scripts/_cli/cmd_versions.ts +2 -1
- package/src/scripts/_cli/detection_report.ts +480 -0
- package/src/scripts/_dispatch.bash +52 -14
- package/src/scripts/_lib/changelog_eras.ts +103 -9
- package/src/scripts/_lib/environment_detector.ts +441 -0
- package/src/scripts/_lib/package_root.ts +72 -0
- package/src/scripts/ai_council/config.ts +15 -7
- package/src/scripts/ai_council/modes.ts +68 -2
- package/src/scripts/ai_council/transport_resolver.ts +331 -0
- package/src/scripts/check_bundle_path_leakage.ts +10 -4
- package/src/scripts/check_dependency_floors.ts +153 -0
- package/src/scripts/check_pack_size.ts +188 -0
- package/src/scripts/council_cli.ts +6 -2
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/lint_eval_fixture_citations.ts +113 -0
- package/src/scripts/lint_ui_stack_bundles.ts +170 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_pack_size — enforce the published-tarball payload budget
|
|
4
|
+
* (road-to-zero-ceremony-install Phase 4).
|
|
5
|
+
*
|
|
6
|
+
* Two metrics, both ungated before this script existed:
|
|
7
|
+
* 1. COMPRESSED tarball size — absolute max, plus the >regression_pct creep
|
|
8
|
+
* rule (fails even under the absolute budget, so slow rot cannot hide
|
|
9
|
+
* under a generous cap).
|
|
10
|
+
* 2. PER-SKILL share of the skills subtree — no single skill may silently
|
|
11
|
+
* reclaim space freed elsewhere. Named exceptions carry a reason.
|
|
12
|
+
*
|
|
13
|
+
* The UNPACKED size is deliberately NOT re-gated here: it is already owned by
|
|
14
|
+
* `evaluator-budgets.unpacked_size_mb`, measured at release time on the BUILT
|
|
15
|
+
* artifact. Two gates for one lever is how conflicting numbers start.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ./scripts-run src/scripts/check_pack_size [--json]
|
|
19
|
+
* ./scripts-run src/scripts/check_pack_size --pack-json <file> # test/offline
|
|
20
|
+
*
|
|
21
|
+
* Exit codes: 0 green · 1 over budget · 2 misuse / unreadable input.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { execFileSync } from 'node:child_process';
|
|
25
|
+
import * as fs from 'node:fs';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
30
|
+
const BUDGET_PATH = path.join(REPO_ROOT, 'src', 'config', 'pack-size-budget.json');
|
|
31
|
+
const SKILL_PREFIX = 'dist/agent-src/skills/';
|
|
32
|
+
|
|
33
|
+
export interface PackFile {
|
|
34
|
+
path: string;
|
|
35
|
+
size: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PackResult {
|
|
39
|
+
size: number;
|
|
40
|
+
unpackedSize: number;
|
|
41
|
+
files: PackFile[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PackSizeBudget {
|
|
45
|
+
regression_pct: number;
|
|
46
|
+
budgets: Record<string, { max: number; last_measured: number; method: string }>;
|
|
47
|
+
per_skill_share: {
|
|
48
|
+
max_pct: number;
|
|
49
|
+
basis: string;
|
|
50
|
+
rationale: string;
|
|
51
|
+
exceptions: Record<string, { max_pct: number; measured_pct: number; reason: string }>;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse `npm pack --json` stdout. npm runs the `prepare` lifecycle script even
|
|
57
|
+
* under `--ignore-scripts` on some versions, and this repo's `prepare` prints a
|
|
58
|
+
* banner — straight into the stream we parse. Slice from the first `[` so a
|
|
59
|
+
* lifecycle banner is tolerated instead of becoming a SyntaxError.
|
|
60
|
+
*/
|
|
61
|
+
export function parsePackJson(stdout: string): PackResult {
|
|
62
|
+
const start = stdout.indexOf('[');
|
|
63
|
+
const parsed = JSON.parse(start >= 0 ? stdout.slice(start) : stdout) as PackResult[];
|
|
64
|
+
const first = parsed[0];
|
|
65
|
+
if (first === undefined) throw new Error('npm pack --json returned an empty array');
|
|
66
|
+
return first;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Bytes per skill under `dist/agent-src/skills/`, plus the subtree total. */
|
|
70
|
+
export function skillBytes(files: readonly PackFile[]): { perSkill: Record<string, number>; total: number } {
|
|
71
|
+
const perSkill: Record<string, number> = {};
|
|
72
|
+
let total = 0;
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
if (!file.path.startsWith(SKILL_PREFIX)) continue;
|
|
75
|
+
const name = file.path.slice(SKILL_PREFIX.length).split('/')[0];
|
|
76
|
+
if (name === undefined || name === '') continue;
|
|
77
|
+
perSkill[name] = (perSkill[name] ?? 0) + file.size;
|
|
78
|
+
total += file.size;
|
|
79
|
+
}
|
|
80
|
+
return { perSkill, total };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every violation, as human-readable lines. Empty array means green. */
|
|
84
|
+
export function evaluate(budget: PackSizeBudget, pack: PackResult): string[] {
|
|
85
|
+
const errors: string[] = [];
|
|
86
|
+
|
|
87
|
+
const packedMb = pack.size / 1e6;
|
|
88
|
+
const entry = budget.budgets['packed_size_mb'];
|
|
89
|
+
if (entry === undefined) {
|
|
90
|
+
errors.push('packed_size_mb: missing from the budget file — gate would pass vacuously');
|
|
91
|
+
} else if (packedMb > entry.max) {
|
|
92
|
+
errors.push(`packed_size_mb: measured ${packedMb.toFixed(3)} exceeds budget ${entry.max}`);
|
|
93
|
+
} else {
|
|
94
|
+
const pct = budget.regression_pct;
|
|
95
|
+
const ceiling = entry.last_measured * (1 + pct / 100);
|
|
96
|
+
if (entry.last_measured > 0 && packedMb > ceiling) {
|
|
97
|
+
errors.push(
|
|
98
|
+
`packed_size_mb: measured ${packedMb.toFixed(3)} regressed >${pct}% vs ` +
|
|
99
|
+
`last_measured ${entry.last_measured} (ceiling ${ceiling.toFixed(3)}) — ` +
|
|
100
|
+
'fails even under the absolute budget',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { perSkill, total } = skillBytes(pack.files);
|
|
106
|
+
if (total === 0) {
|
|
107
|
+
errors.push(`per_skill_share: no files under ${SKILL_PREFIX} — gate would pass vacuously`);
|
|
108
|
+
return errors;
|
|
109
|
+
}
|
|
110
|
+
const { max_pct: defaultMax, exceptions } = budget.per_skill_share;
|
|
111
|
+
for (const [name, bytes] of Object.entries(perSkill)) {
|
|
112
|
+
const share = (bytes / total) * 100;
|
|
113
|
+
const exception = exceptions[name];
|
|
114
|
+
const cap = exception?.max_pct ?? defaultMax;
|
|
115
|
+
if (share > cap) {
|
|
116
|
+
errors.push(
|
|
117
|
+
exception === undefined
|
|
118
|
+
? `per_skill_share: ${name} is ${share.toFixed(2)}% of the skills payload, over the ${cap}% cap — ` +
|
|
119
|
+
'shrink it, or add a named exception with a reason'
|
|
120
|
+
: `per_skill_share: ${name} is ${share.toFixed(2)}%, over its own exception cap of ${cap}% — ` +
|
|
121
|
+
'the exception is not a blank cheque; shrink it or raise the cap with a reason',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const name of Object.keys(exceptions)) {
|
|
126
|
+
if (!(name in perSkill)) {
|
|
127
|
+
errors.push(`per_skill_share: exception for '${name}' is stale — no such skill ships`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return errors;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readPack(argv: readonly string[]): PackResult {
|
|
134
|
+
const idx = argv.indexOf('--pack-json');
|
|
135
|
+
if (idx >= 0) {
|
|
136
|
+
const file = argv[idx + 1];
|
|
137
|
+
if (file === undefined) throw new Error('--pack-json needs a path');
|
|
138
|
+
return parsePackJson(fs.readFileSync(file, 'utf-8'));
|
|
139
|
+
}
|
|
140
|
+
const stdout = execFileSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts', '--silent'], {
|
|
141
|
+
cwd: REPO_ROOT,
|
|
142
|
+
encoding: 'utf-8',
|
|
143
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
144
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
145
|
+
});
|
|
146
|
+
return parsePackJson(stdout);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
150
|
+
let budget: PackSizeBudget;
|
|
151
|
+
let pack: PackResult;
|
|
152
|
+
try {
|
|
153
|
+
budget = JSON.parse(fs.readFileSync(BUDGET_PATH, 'utf-8')) as PackSizeBudget;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
process.stderr.write(`❌ pack size: cannot read ${BUDGET_PATH}: ${String(err)}\n`);
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
pack = readPack(argv);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
process.stderr.write(`❌ pack size: npm pack failed: ${String(err)}\n`);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const errors = evaluate(budget, pack);
|
|
166
|
+
const { perSkill, total } = skillBytes(pack.files);
|
|
167
|
+
if (argv.includes('--json')) {
|
|
168
|
+
process.stdout.write(
|
|
169
|
+
`${JSON.stringify({ packed_mb: pack.size / 1e6, skills_total_bytes: total, skills: Object.keys(perSkill).length, errors }, null, 2)}\n`,
|
|
170
|
+
);
|
|
171
|
+
return errors.length > 0 ? 1 : 0;
|
|
172
|
+
}
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
for (const e of errors) process.stderr.write(`❌ pack size: ${e}\n`);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
process.stdout.write(
|
|
178
|
+
`✅ pack size within budget — ${(pack.size / 1e6).toFixed(3)} MB packed, ` +
|
|
179
|
+
`${Object.keys(perSkill).length} skills, largest share ` +
|
|
180
|
+
`${Math.max(...Object.values(perSkill).map((b) => (b / total) * 100)).toFixed(2)}%\n`,
|
|
181
|
+
);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const _selfPath = fileURLToPath(import.meta.url);
|
|
186
|
+
if (process.argv[1] !== undefined && path.resolve(process.argv[1]) === path.resolve(_selfPath)) {
|
|
187
|
+
process.exit(main());
|
|
188
|
+
}
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
resolve_config_path,
|
|
70
70
|
} from './ai_council/config.js';
|
|
71
71
|
import { AuthCache, select_solo_member } from './ai_council/solo_dispatch.js';
|
|
72
|
-
import { InvalidModeError, resolve_mode } from './ai_council/modes.js';
|
|
72
|
+
import { InvalidModeError, resolve_global_mode, resolve_mode } from './ai_council/modes.js';
|
|
73
73
|
import { appendEvent } from './ai_council/events_log.js';
|
|
74
74
|
import {
|
|
75
75
|
type ClassificationResult,
|
|
@@ -564,7 +564,11 @@ function build_members(settings: Dict, opts: BuildMembersOptions = {}): External
|
|
|
564
564
|
);
|
|
565
565
|
}
|
|
566
566
|
const members_cfg = (ai['members'] as Dict) || {};
|
|
567
|
-
|
|
567
|
+
// Accepts BOTH the synthesized flat `mode` and the raw `defaults.mode`
|
|
568
|
+
// shape — see modes.ts::resolve_global_mode. Reading only the flat key
|
|
569
|
+
// dropped the configured default for every caller that hands this
|
|
570
|
+
// exported function a raw `.ai-council.yml` dict.
|
|
571
|
+
const global_mode = resolve_global_mode(ai);
|
|
568
572
|
const cli_budget_cfg = _isDict(ai) ? ((ai['cli_call_budget'] as Dict) || {}) : {};
|
|
569
573
|
const cli_caps = _isDict(cli_budget_cfg)
|
|
570
574
|
? ((cli_budget_cfg['max_calls_per_day'] as Dict) || {})
|
|
@@ -65,11 +65,38 @@ UNPACKED_BYTES="$(npm pack --dry-run --json --ignore-scripts --silent 2>/dev/nul
|
|
|
65
65
|
step "headless consumer install from the tarball"
|
|
66
66
|
WORK="$(mktemp -d)"
|
|
67
67
|
trap 'rm -rf "$WORK"' EXIT
|
|
68
|
+
COLD_START_T0="$(node -e 'console.log(Date.now())')"
|
|
68
69
|
( cd "$WORK" && npm init -y >/dev/null 2>&1 \
|
|
69
70
|
&& npm install --omit=dev --no-audit --no-fund --silent "$ROOT/$TARBALL" )
|
|
70
71
|
PKG_DIR="$WORK/node_modules/@event4u/agent-config"
|
|
71
72
|
[[ -d "$PKG_DIR" ]] || { echo "❌ install did not produce the package dir" >&2; exit 1; }
|
|
72
73
|
|
|
74
|
+
# ------------------------------------------- 3b. cold start: install → doctor
|
|
75
|
+
# The bare-machine wall-clock a first-time user lives through, measured end to
|
|
76
|
+
# end from `npm install` to `doctor` having run. This is EVIDENCE, never a
|
|
77
|
+
# promise, and deliberately NOT a budget entry: it is dominated by network and
|
|
78
|
+
# registry latency, which are not ours to guarantee. A gate on it would flap on
|
|
79
|
+
# runner weather and teach the reader to ignore the line
|
|
80
|
+
# (road-to-zero-ceremony-install Phase 6).
|
|
81
|
+
step "cold start: install → doctor (evidence, not a gate)"
|
|
82
|
+
COLD_START_MS="$(node -e '
|
|
83
|
+
const {execFileSync}=require("node:child_process");
|
|
84
|
+
const [bin, t0] = process.argv.slice(1);
|
|
85
|
+
try { execFileSync(bin, ["doctor"], {stdio:"ignore"}); }
|
|
86
|
+
catch { /* doctor may report findings; the run still bounds the wall-clock */ }
|
|
87
|
+
console.log(Date.now() - Number(t0));' "$WORK/node_modules/.bin/agent-config" "$COLD_START_T0")"
|
|
88
|
+
COLD_START_CONDITIONS="$(uname -s)/$(uname -m) · node $(node --version) · npm $(npm --version) · npm install --omit=dev from a LOCAL tarball (no registry round-trip for the package itself; a real first-touch install adds registry latency)"
|
|
89
|
+
printf 'cold_start_install_to_doctor_ms=%s\n' "$COLD_START_MS"
|
|
90
|
+
printf 'cold_start_conditions=%s\n' "$COLD_START_CONDITIONS"
|
|
91
|
+
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
|
|
92
|
+
{
|
|
93
|
+
printf '### Cold start — install → doctor\n\n'
|
|
94
|
+
printf '**%s ms**\n\n' "$COLD_START_MS"
|
|
95
|
+
printf 'Conditions: %s\n\n' "$COLD_START_CONDITIONS"
|
|
96
|
+
printf 'Evidence, not a promise — no shipped claim states this as a guarantee.\n'
|
|
97
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
98
|
+
fi
|
|
99
|
+
|
|
73
100
|
# --------------------------------------------------------- 4. staleness lints
|
|
74
101
|
step "staleness lints (pre-migration refs)"
|
|
75
102
|
./scripts-run src/scripts/lint_pre_migration_refs
|
|
@@ -128,10 +155,16 @@ const m={
|
|
|
128
155
|
mcp_boot_to_initialize_ms: Number(process.argv[6]),
|
|
129
156
|
cli_help_command_count: Number(process.argv[7]),
|
|
130
157
|
mcp_public_tool_count: Number(process.argv[8]),
|
|
158
|
+
// Evidence-only: carried in the artifact with its conditions, never gated.
|
|
159
|
+
// check_evaluator_budgets iterates the BUDGET entries, so an extra
|
|
160
|
+
// measurement is recorded and ignored by the gate — by design.
|
|
161
|
+
cold_start_install_to_doctor_ms: Number(process.argv[9]),
|
|
162
|
+
cold_start_conditions: process.argv[10],
|
|
131
163
|
};
|
|
132
164
|
fs.writeFileSync(process.argv[1], JSON.stringify(m, null, 2));
|
|
133
165
|
console.log(JSON.stringify(m, null, 1));' \
|
|
134
|
-
"$MEASURE_TMP" "$UNPACKED_BYTES" "$NM_MB" "$DEP_COUNT" "$CLI_MS" "$MCP_MS" "$CLI_COUNT" "$MCP_TOOLS"
|
|
166
|
+
"$MEASURE_TMP" "$UNPACKED_BYTES" "$NM_MB" "$DEP_COUNT" "$CLI_MS" "$MCP_MS" "$CLI_COUNT" "$MCP_TOOLS" \
|
|
167
|
+
"$COLD_START_MS" "$COLD_START_CONDITIONS"
|
|
135
168
|
|
|
136
169
|
step "budget gate (absolute + >regression_pct creep)"
|
|
137
170
|
./scripts-run src/scripts/check_evaluator_budgets --measurements "$MEASURE_TMP"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Every design-artifact eval fixture id must be cited by some surface.
|
|
4
|
+
*
|
|
5
|
+
* The fixtures file used to assert that the lifecycle contract's branch table
|
|
6
|
+
* "references these ids". It cites nine of them; the rest are gated elsewhere
|
|
7
|
+
* (the design-fidelity mechanics guideline, the lane-matrix test) — and a
|
|
8
|
+
* handful were cited by nothing at all. An uncited fixture is an eval nobody
|
|
9
|
+
* runs and no contract depends on, which is exactly the shape of coverage that
|
|
10
|
+
* looks real in a count and is absent in practice.
|
|
11
|
+
*
|
|
12
|
+
* This checks citation, not correctness: an id must appear outside its own
|
|
13
|
+
* definition. Which surface cites it is the author's call.
|
|
14
|
+
*
|
|
15
|
+
* Exit codes: 0 every id cited, 1 one or more uncited.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const QUIET = process.argv.includes('--quiet');
|
|
23
|
+
|
|
24
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
25
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
26
|
+
const FIXTURES = path.join(REPO, 'tests', 'design-artifacts', 'eval-fixtures.md');
|
|
27
|
+
|
|
28
|
+
/** Surfaces allowed to satisfy a citation. */
|
|
29
|
+
const SCAN_ROOTS: readonly string[] = [
|
|
30
|
+
path.join(REPO, 'docs', 'contracts'),
|
|
31
|
+
path.join(REPO, 'docs', 'guidelines'),
|
|
32
|
+
path.join(REPO, 'src', 'skills'),
|
|
33
|
+
path.join(REPO, 'src', 'rules'),
|
|
34
|
+
path.join(REPO, 'tests', 'scripts'),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const SCAN_EXTS: ReadonlySet<string> = new Set(['.md', '.ts']);
|
|
38
|
+
|
|
39
|
+
function walk(dir: string, out: string[]): void {
|
|
40
|
+
let entries: fs.Dirent[];
|
|
41
|
+
try {
|
|
42
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const p = path.join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
walk(p, out);
|
|
50
|
+
} else if (SCAN_EXTS.has(path.extname(entry.name))) {
|
|
51
|
+
out.push(p);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function main(): number {
|
|
57
|
+
let fixtureText: string;
|
|
58
|
+
try {
|
|
59
|
+
fixtureText = fs.readFileSync(FIXTURES, { encoding: 'utf-8' });
|
|
60
|
+
} catch {
|
|
61
|
+
process.stderr.write(`lint-eval-fixture-citations: cannot read ${FIXTURES}\n`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Ids are declared as `### daf-...` headings or as leading table cells.
|
|
66
|
+
const ids = new Set<string>();
|
|
67
|
+
for (const m of fixtureText.matchAll(/^###\s+(daf-[a-z0-9-]+)\s*$/gm)) {
|
|
68
|
+
ids.add(m[1] as string);
|
|
69
|
+
}
|
|
70
|
+
for (const m of fixtureText.matchAll(/^\|\s*`(daf-[a-z0-9-]+)`\s*\|/gm)) {
|
|
71
|
+
ids.add(m[1] as string);
|
|
72
|
+
}
|
|
73
|
+
if (ids.size === 0) {
|
|
74
|
+
process.stderr.write('lint-eval-fixture-citations: no fixture ids found — parser drift.\n');
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const files: string[] = [];
|
|
79
|
+
for (const root of SCAN_ROOTS) {
|
|
80
|
+
walk(root, files);
|
|
81
|
+
}
|
|
82
|
+
const haystack = files
|
|
83
|
+
.map((f) => {
|
|
84
|
+
try {
|
|
85
|
+
return fs.readFileSync(f, { encoding: 'utf-8' });
|
|
86
|
+
} catch {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.join('\n');
|
|
91
|
+
|
|
92
|
+
const uncited = [...ids].filter((id) => !haystack.includes(id)).sort();
|
|
93
|
+
if (uncited.length > 0) {
|
|
94
|
+
for (const id of uncited) {
|
|
95
|
+
process.stdout.write(
|
|
96
|
+
`lint-eval-fixture-citations: \`${id}\` is cited by no contract, ` +
|
|
97
|
+
'guideline, skill, rule, or test — an eval nobody runs.\n',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
process.stderr.write(
|
|
101
|
+
`lint-eval-fixture-citations: ${uncited.length} of ${ids.size} fixture id(s) uncited.\n`,
|
|
102
|
+
);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
if (!QUIET) {
|
|
106
|
+
process.stdout.write(
|
|
107
|
+
`lint-eval-fixture-citations: ${ids.size} fixture id(s), all cited.\n`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.exit(main());
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Lint the UI-track verb → skill-bundle map.
|
|
4
|
+
*
|
|
5
|
+
* The stack-dispatched UI steps halt with a directive verb
|
|
6
|
+
* (`ui-apply-<stack>` and friends). Those verbs are agent-interpreted, not
|
|
7
|
+
* skill paths — only 2 of the engine's 11 literal verbs name a real skill — so
|
|
8
|
+
* the mapping from verb to the skills that implement it is a separate fact.
|
|
9
|
+
* It used to live only in a prose table in `docs/contracts/ui-track-flow.md`,
|
|
10
|
+
* unverified, and two of its four rows were wrong: `vue` named the verb itself
|
|
11
|
+
* (no skill at all) and `plain` — the fallback for every unrecognised project —
|
|
12
|
+
* named `blade-ui`, a `packs: [laravel]` skill that a non-Laravel consumer does
|
|
13
|
+
* not have installed.
|
|
14
|
+
*
|
|
15
|
+
* Two assertions, both decidable in this repo:
|
|
16
|
+
*
|
|
17
|
+
* 1. RESOLUTION — every bundle member is a real skill (`src/skills/<n>/SKILL.md`).
|
|
18
|
+
* 2. PACK REACH — a lane declared `pack_agnostic` names only skills that ship
|
|
19
|
+
* in a stack-neutral pack. This is the assertion that actually matters: CI
|
|
20
|
+
* here cannot see a consumer's installed pack combination, so "the skill
|
|
21
|
+
* exists in our tree" would have passed the `plain → blade-ui` defect
|
|
22
|
+
* unchanged.
|
|
23
|
+
*
|
|
24
|
+
* Exit codes: 0 clean, 1 one or more findings.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
STACK_BUNDLES,
|
|
33
|
+
type StackBundle,
|
|
34
|
+
} from '../agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.js';
|
|
35
|
+
|
|
36
|
+
const QUIET = process.argv.includes('--quiet');
|
|
37
|
+
|
|
38
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
39
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
40
|
+
const SKILLS_DIR = path.join(REPO, 'src', 'skills');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Packs that ship regardless of the project's framework. A `pack_agnostic`
|
|
44
|
+
* lane may only draw on these — anything else is unavailable to some consumer.
|
|
45
|
+
*/
|
|
46
|
+
const STACK_NEUTRAL_PACKS: ReadonlySet<string> = new Set(['engineering-base', 'core', 'meta']);
|
|
47
|
+
|
|
48
|
+
const findings: string[] = [];
|
|
49
|
+
|
|
50
|
+
function skill_path(name: string): string {
|
|
51
|
+
return path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Return the `packs:` list from a skill's frontmatter, or `null` if absent. */
|
|
55
|
+
function packs_of(name: string): string[] | null {
|
|
56
|
+
let text: string;
|
|
57
|
+
try {
|
|
58
|
+
text = fs.readFileSync(skill_path(name), { encoding: 'utf-8' });
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
// Frontmatter only — a `packs:` mention in the body is not a declaration.
|
|
63
|
+
const end = text.indexOf('\n---', 4);
|
|
64
|
+
const front = end === -1 ? text : text.slice(0, end);
|
|
65
|
+
// `[ \t]*`, not `\s*`: `\s` matches a newline, so the greedy form swallowed
|
|
66
|
+
// the line break and captured the first block-list item as if it were an
|
|
67
|
+
// inline value — which made every block-list skill parse as "no packs".
|
|
68
|
+
const match = /^packs:[ \t]*(.*)$/m.exec(front);
|
|
69
|
+
if (match === null) return null;
|
|
70
|
+
const inline = (match[1] ?? '').trim();
|
|
71
|
+
if (inline.startsWith('[')) {
|
|
72
|
+
return inline
|
|
73
|
+
.replace(/^\[|\]$/g, '')
|
|
74
|
+
.split(',')
|
|
75
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
76
|
+
.filter((s) => s !== '');
|
|
77
|
+
}
|
|
78
|
+
// Block list: consume the ` - name` lines that follow. `match[0]` stops
|
|
79
|
+
// before the newline, so the first split element is the empty tail of the
|
|
80
|
+
// matched line itself — dropping it is required, not cosmetic: treating it
|
|
81
|
+
// as a non-item ended the loop immediately and made every block-list skill
|
|
82
|
+
// parse as "no packs", which is how this linter first shipped green against
|
|
83
|
+
// the very defect it exists to catch.
|
|
84
|
+
const rest = front.slice(match.index + match[0].length).split('\n').slice(1);
|
|
85
|
+
const out: string[] = [];
|
|
86
|
+
for (const line of rest) {
|
|
87
|
+
const item = /^\s*-\s*(.+?)\s*$/.exec(line);
|
|
88
|
+
if (item === null) break;
|
|
89
|
+
out.push((item[1] ?? '').replace(/^['"]|['"]$/g, ''));
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function check_lane(lane: string, bundle: StackBundle): void {
|
|
95
|
+
const members = [...bundle.build, ...bundle.review];
|
|
96
|
+
if (members.length === 0) {
|
|
97
|
+
findings.push(`${lane}: bundle is empty — a lane must name at least one skill.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const name of new Set(members)) {
|
|
101
|
+
// 1. RESOLUTION
|
|
102
|
+
if (!fs.existsSync(skill_path(name))) {
|
|
103
|
+
findings.push(
|
|
104
|
+
`${lane}: bundle member \`${name}\` has no ` +
|
|
105
|
+
`\`src/skills/${name}/SKILL.md\` — the verb would resolve to nothing.`,
|
|
106
|
+
);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// 2. PACK REACH
|
|
110
|
+
if (!bundle.pack_agnostic) continue;
|
|
111
|
+
const packs = packs_of(name);
|
|
112
|
+
if (packs === null) {
|
|
113
|
+
findings.push(
|
|
114
|
+
`${lane}: bundle member \`${name}\` declares no \`packs:\` — ` +
|
|
115
|
+
'a pack-agnostic lane cannot verify it ships everywhere.',
|
|
116
|
+
);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const offenders = packs.filter((p) => !STACK_NEUTRAL_PACKS.has(p));
|
|
120
|
+
if (offenders.length > 0) {
|
|
121
|
+
findings.push(
|
|
122
|
+
`${lane}: lane is \`pack_agnostic\` but member \`${name}\` ships only in ` +
|
|
123
|
+
`[${offenders.join(', ')}] — a consumer without that pack gets nothing. ` +
|
|
124
|
+
'This is the `plain → blade-ui` defect class.',
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function main(): number {
|
|
131
|
+
const lanes = Object.keys(STACK_BUNDLES).sort();
|
|
132
|
+
if (lanes.length === 0) {
|
|
133
|
+
process.stderr.write('lint-ui-stack-bundles: STACK_BUNDLES is empty — nothing to check.\n');
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
for (const lane of lanes) {
|
|
137
|
+
check_lane(lane, STACK_BUNDLES[lane] as StackBundle);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A lane usable as a fallback must be pack-agnostic, else the fallback
|
|
141
|
+
// itself is the thing that fails for an unpredictable consumer.
|
|
142
|
+
const plain = STACK_BUNDLES['plain'];
|
|
143
|
+
if (plain === undefined) {
|
|
144
|
+
findings.push('plain: missing — it is the documented DEFAULT_DIRECTIVE lane.');
|
|
145
|
+
} else if (!plain.pack_agnostic) {
|
|
146
|
+
findings.push(
|
|
147
|
+
'plain: the fallback lane must be `pack_agnostic` — every unrecognised ' +
|
|
148
|
+
'project lands here and its packs are unknown.',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (findings.length > 0) {
|
|
153
|
+
for (const f of findings) {
|
|
154
|
+
process.stdout.write(`lint-ui-stack-bundles: ${f}\n`);
|
|
155
|
+
}
|
|
156
|
+
process.stderr.write(
|
|
157
|
+
`lint-ui-stack-bundles: ${findings.length} finding(s) across ${lanes.length} lane(s).\n`,
|
|
158
|
+
);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
if (!QUIET) {
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
`lint-ui-stack-bundles: ${lanes.length} lane(s) clean — every bundle member ` +
|
|
164
|
+
'resolves, and every pack-agnostic lane stays stack-neutral.\n',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.exit(main());
|