@event4u/agent-config 9.10.0 → 9.12.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 +111 -0
- package/README.md +20 -16
- package/agents/templates/.ai-council.yml.example +481 -0
- package/agents/templates/.ai-video.xml.example +247 -0
- package/dist/agent-src/commands/fix/ci.md +29 -0
- package/dist/agent-src/commands/pr/create.md +60 -0
- package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
- package/dist/agent-src/rules/design-fidelity.md +2 -2
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
- package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
- package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
- package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
- 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/skills/typography-system/SKILL.md +55 -11
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -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 +34 -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 +23 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +24 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +332 -0
- package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +449 -11
- 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-523HCFYO.js +90 -0
- package/dist/cli-delegate/chunk-736JCWLM.js +1104 -0
- package/dist/cli-delegate/chunk-7HQ52DHU.js +74 -0
- package/dist/cli-delegate/chunk-A3HJ6NTI.js +1084 -0
- package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -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-HYWSRNUX.js +179 -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-QFKQLQ7X.js +10835 -0
- package/dist/cli-delegate/chunk-TEGJU2JX.js +6908 -0
- package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
- package/dist/cli-delegate/chunk-XX6TOPD7.js +715 -0
- package/dist/cli-delegate/cmd_analyze_session.js +294 -0
- package/dist/cli-delegate/cmd_conformance.js +643 -0
- package/dist/cli-delegate/cmd_converge.js +326 -0
- package/dist/cli-delegate/cmd_doctor.js +142 -0
- package/dist/cli-delegate/cmd_explain.js +2576 -0
- package/dist/cli-delegate/cmd_export.js +298 -0
- package/dist/cli-delegate/cmd_fleet.js +333 -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 +153 -0
- package/dist/cli-delegate/cmd_prune.js +446 -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 +225 -0
- package/dist/cli-delegate/cmd_sync.js +266 -0
- package/dist/cli-delegate/cmd_uninstall.js +657 -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 +307 -0
- package/dist/cli-delegate/cmd_versions.js +370 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +45 -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 -7
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -4
- 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 +634 -409
- package/dist/install/ruleInScope.js +123 -0
- package/dist/install/ruleInScope.js.map +1 -0
- package/dist/install/rule_scope.js +7 -3
- 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 +12 -3
- package/dist/install/wizard-plan.js.map +1 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1553 -2728
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +6 -22
- package/dist/scripts/condense.js.map +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +14 -0
- package/package.json +4 -2
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +34 -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 +23 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +24 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +332 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +449 -11
- 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/ruleInScope.ts +133 -0
- package/src/install/rule_scope.ts +7 -3
- package/src/install/toolDetection.ts +39 -8
- package/src/install/wizard-plan.ts +13 -3
- 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/global_deploy_inventory.ts +38 -2
- 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_installer_import_purity.ts +295 -0
- package/src/scripts/check_pack_size.ts +188 -0
- package/src/scripts/condense.ts +7 -28
- package/src/scripts/council_cli.ts +6 -2
- package/src/scripts/evaluator_umbrella.sh +34 -1
- package/src/scripts/install-hooks.sh +36 -4
- package/src/scripts/install.ts +210 -5
- package/src/scripts/lint_eval_fixture_citations.ts +113 -0
- package/src/scripts/lint_ui_stack_bundles.ts +201 -0
|
@@ -17,17 +17,35 @@
|
|
|
17
17
|
*
|
|
18
18
|
* 1. `composer.json` lists `livewire/livewire` AND `livewire/flux` →
|
|
19
19
|
* `blade-livewire-flux`
|
|
20
|
-
* 2. `
|
|
20
|
+
* 2. `composer.json` lists `filament/filament` → `filament` (before bare
|
|
21
|
+
* Livewire: Filament pulls Livewire in transitively)
|
|
22
|
+
* 3. `composer.json` lists `livewire/livewire` → `blade-livewire`
|
|
23
|
+
* 4. `package.json` lists `react` AND any of `@radix-ui/*`,
|
|
21
24
|
* `shadcn-ui` or has a `components.json` (the shadcn marker file)
|
|
22
25
|
* → `react-shadcn`
|
|
23
|
-
*
|
|
24
|
-
*
|
|
26
|
+
* 5. `package.json` lists `react` → `react`
|
|
27
|
+
* 6. `package.json` lists `vue` (any major) → `vue`
|
|
28
|
+
* 7. Either manifest names a recognised-but-unmodelled framework
|
|
29
|
+
* (Svelte, Angular, Nuxt, Astro, Solid, Qwik, Inertia) → `unknown`
|
|
30
|
+
* 8. Otherwise → `plain`
|
|
31
|
+
*
|
|
32
|
+
* Rules 3, 5 and 7 exist because the earlier table sent three common
|
|
33
|
+
* shapes into `plain`: Laravel+Livewire without Flux failed rule 1's `&&`,
|
|
34
|
+
* React without Radix failed rule 4, and every unmodelled framework was
|
|
35
|
+
* indistinguishable from a genuinely plain project. `plain` now means "no
|
|
36
|
+
* frontend markers"; `unknown` means "a framework we do not model", and
|
|
37
|
+
* dispatch refuses on it rather than handing over generic tooling silently.
|
|
25
38
|
*
|
|
26
39
|
* Detection is filesystem-cheap: at most three small JSON reads per
|
|
27
40
|
* project root. Errors (missing file, malformed JSON, missing `require`
|
|
28
41
|
* section) downgrade to `plain` rather than raising — a wrong stack
|
|
29
42
|
* label is recoverable (audit catches it, user can override), but a
|
|
30
43
|
* crash mid-dispatch is not.
|
|
44
|
+
*
|
|
45
|
+
* Root manifests only, by design: a monorepo caller passes the workspace
|
|
46
|
+
* root that carries the manifest it cares about. Note the consequence —
|
|
47
|
+
* a caller that always passes the repository root gets `plain` for a
|
|
48
|
+
* monorepo, so the caller owns scope selection, not the detector.
|
|
31
49
|
*/
|
|
32
50
|
import * as fs from 'node:fs';
|
|
33
51
|
import * as path from 'node:path';
|
|
@@ -43,18 +61,59 @@ import * as path from 'node:path';
|
|
|
43
61
|
*/
|
|
44
62
|
export const KNOWN_STACKS: ReadonlySet<string> = new Set([
|
|
45
63
|
'blade-livewire-flux',
|
|
64
|
+
'blade-livewire',
|
|
65
|
+
'filament',
|
|
46
66
|
'react-shadcn',
|
|
67
|
+
'react',
|
|
47
68
|
'vue',
|
|
48
69
|
'plain',
|
|
70
|
+
'unknown',
|
|
49
71
|
]);
|
|
50
72
|
|
|
51
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Label for a project with no frontend markers at all.
|
|
75
|
+
*
|
|
76
|
+
* `plain` used to carry two meanings — "genuinely a plain HTML/Tailwind
|
|
77
|
+
* project" and "detection failed, here is generic tooling anyway" — which made
|
|
78
|
+
* a loud failure impossible without punishing real plain projects. The second
|
|
79
|
+
* meaning is now {@link UNSUPPORTED_STACK}.
|
|
80
|
+
*/
|
|
52
81
|
export const DEFAULT_STACK = 'plain';
|
|
53
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Label for a project whose frontend IS recognisable but is not modelled.
|
|
85
|
+
*
|
|
86
|
+
* Dispatch refuses on this label instead of degrading to generic tooling: a
|
|
87
|
+
* Filament or Svelte project silently routed to a Tailwind-only bundle is
|
|
88
|
+
* user-hostile precisely because the output looks like it worked.
|
|
89
|
+
*/
|
|
90
|
+
export const UNSUPPORTED_STACK = 'unknown';
|
|
91
|
+
|
|
54
92
|
const _SHADCN_RADIX_PREFIX = '@radix-ui/';
|
|
55
93
|
const _SHADCN_PACKAGE_NAMES: ReadonlySet<string> = new Set(['shadcn-ui', 'shadcn']);
|
|
56
94
|
const _FLUX_PACKAGE = 'livewire/flux';
|
|
57
95
|
const _LIVEWIRE_PACKAGE = 'livewire/livewire';
|
|
96
|
+
const _FILAMENT_PACKAGE = 'filament/filament';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Frontend markers this package recognises but does not model.
|
|
100
|
+
*
|
|
101
|
+
* One of these present means detection **succeeded** at recognising a
|
|
102
|
+
* framework and the package simply has no lane for it — the `unknown` case,
|
|
103
|
+
* not the `plain` case. Keep the list short and factual; a marker leaves it
|
|
104
|
+
* only when a real lane exists.
|
|
105
|
+
*/
|
|
106
|
+
const _UNMODELLED_MARKERS: ReadonlyArray<string> = [
|
|
107
|
+
'svelte',
|
|
108
|
+
'@angular/core',
|
|
109
|
+
'nuxt',
|
|
110
|
+
'astro',
|
|
111
|
+
'solid-js',
|
|
112
|
+
'@builder.io/qwik',
|
|
113
|
+
'@inertiajs/vue3',
|
|
114
|
+
'@inertiajs/react',
|
|
115
|
+
'inertiajs/inertia-laravel',
|
|
116
|
+
];
|
|
58
117
|
|
|
59
118
|
/**
|
|
60
119
|
* A heterogeneous JSON object, mirroring a Python `dict[str, object]`.
|
|
@@ -63,6 +122,49 @@ const _LIVEWIRE_PACKAGE = 'livewire/livewire';
|
|
|
63
122
|
*/
|
|
64
123
|
type Manifest = { [key: string]: unknown };
|
|
65
124
|
|
|
125
|
+
/**
|
|
126
|
+
* The independent axes a frontend stack actually varies along.
|
|
127
|
+
*
|
|
128
|
+
* A flat label forces all-or-nothing matches: `livewire ∧ flux` was never the
|
|
129
|
+
* bug, it was the symptom of collapsing three axes into one enum value. The
|
|
130
|
+
* measured proof is Nuxt — `nuxt` is on the unmodelled-marker list, but the
|
|
131
|
+
* project is labelled `vue` because Vue is a Nuxt dependency and matched first.
|
|
132
|
+
* No ordering of one list can express "Nuxt implies Vue but is not Vue".
|
|
133
|
+
*
|
|
134
|
+
* Axes are **additive**: the flat `frontend` label keeps its existing
|
|
135
|
+
* computation untouched, so the eight shipped labels are byte-stable by
|
|
136
|
+
* construction rather than by test. Dispatch composition reads the axes.
|
|
137
|
+
*
|
|
138
|
+
* `'none'` means "this axis is genuinely absent" (a Blade app has no JS
|
|
139
|
+
* component library); `'unknown'` means "not recognised", which is a different
|
|
140
|
+
* fact and must not be conflated with absence.
|
|
141
|
+
*/
|
|
142
|
+
export interface StackAxes {
|
|
143
|
+
/** Template / view layer — what markup is authored in. */
|
|
144
|
+
readonly view: string;
|
|
145
|
+
/** Reactivity layer — what drives updates. */
|
|
146
|
+
readonly reactivity: string;
|
|
147
|
+
/** Component library on top of the reactivity layer. */
|
|
148
|
+
readonly component_lib: string;
|
|
149
|
+
/** Styling system. */
|
|
150
|
+
readonly css: string;
|
|
151
|
+
/**
|
|
152
|
+
* Meta-framework wrapping the reactivity layer (Nuxt over Vue, Next over
|
|
153
|
+
* React, Astro over anything). Its own axis precisely because it does not
|
|
154
|
+
* replace the layer underneath — which is what the flat label got wrong.
|
|
155
|
+
*/
|
|
156
|
+
readonly meta: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Every axis unresolved — the shape for a project with no signals at all. */
|
|
160
|
+
const _EMPTY_AXES: StackAxes = {
|
|
161
|
+
view: 'none',
|
|
162
|
+
reactivity: 'none',
|
|
163
|
+
component_lib: 'none',
|
|
164
|
+
css: 'none',
|
|
165
|
+
meta: 'none',
|
|
166
|
+
};
|
|
167
|
+
|
|
66
168
|
/**
|
|
67
169
|
* Outcome of one detection pass.
|
|
68
170
|
*
|
|
@@ -79,10 +181,28 @@ type Manifest = { [key: string]: unknown };
|
|
|
79
181
|
export class StackResult {
|
|
80
182
|
readonly frontend: string;
|
|
81
183
|
readonly mtime: number;
|
|
184
|
+
/** Multi-axis view of the same detection. See {@link StackAxes}. */
|
|
185
|
+
readonly axes: StackAxes;
|
|
186
|
+
/**
|
|
187
|
+
* Conflicting signals on one axis, e.g. `['reactivity: react + vue']`.
|
|
188
|
+
*
|
|
189
|
+
* Non-empty means the project is genuinely two things and the detector
|
|
190
|
+
* refuses to pick. Guessing a priority order is the worse property for a
|
|
191
|
+
* global package: asking costs one turn, a wrong silent pick costs the
|
|
192
|
+
* whole run.
|
|
193
|
+
*/
|
|
194
|
+
readonly ambiguity: ReadonlyArray<string>;
|
|
82
195
|
|
|
83
|
-
constructor(args: {
|
|
196
|
+
constructor(args: {
|
|
197
|
+
frontend: string;
|
|
198
|
+
mtime: number;
|
|
199
|
+
axes?: StackAxes;
|
|
200
|
+
ambiguity?: ReadonlyArray<string>;
|
|
201
|
+
}) {
|
|
84
202
|
this.frontend = args.frontend;
|
|
85
203
|
this.mtime = args.mtime;
|
|
204
|
+
this.axes = args.axes ?? _EMPTY_AXES;
|
|
205
|
+
this.ambiguity = args.ambiguity ?? [];
|
|
86
206
|
}
|
|
87
207
|
}
|
|
88
208
|
|
|
@@ -105,19 +225,97 @@ export function detect_stack(project_root: string): StackResult {
|
|
|
105
225
|
const components_json = _is_file(path.join(project_root, 'components.json'));
|
|
106
226
|
const mtime = latest_manifest_mtime(project_root);
|
|
107
227
|
|
|
228
|
+
// Axes are computed once and attached to every result below. The label
|
|
229
|
+
// chain that follows is UNCHANGED — that is deliberate, so the eight
|
|
230
|
+
// shipped labels stay byte-stable by construction rather than by test.
|
|
231
|
+
// Composition reads `axes`; only the ambiguity guard alters a label.
|
|
232
|
+
const axes = _detect_axes(composer, pkg, components_json, project_root);
|
|
233
|
+
const ambiguity = _detect_ambiguity(composer, pkg);
|
|
234
|
+
const _r = (frontend: string): StackResult =>
|
|
235
|
+
new StackResult({ frontend, mtime, axes, ambiguity });
|
|
236
|
+
|
|
237
|
+
// Conflicting signals on one axis: refuse rather than let the priority
|
|
238
|
+
// chain below silently pick a winner. Measured baseline: react + vue in
|
|
239
|
+
// one manifest resolved to `react` with no warning at all.
|
|
240
|
+
if (ambiguity.length > 0) {
|
|
241
|
+
return _r(UNSUPPORTED_STACK);
|
|
242
|
+
}
|
|
243
|
+
|
|
108
244
|
if (_is_blade_livewire_flux(composer)) {
|
|
109
|
-
return
|
|
245
|
+
return _r('blade-livewire-flux');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Filament before bare Livewire: it pulls Livewire in transitively, so a
|
|
249
|
+
// Filament project would otherwise be labelled by its dependency rather
|
|
250
|
+
// than by the framework the developer actually works in.
|
|
251
|
+
if (_has_package(composer, ['require', 'require-dev'], _FILAMENT_PACKAGE)) {
|
|
252
|
+
return _r('filament');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Livewire WITHOUT Flux is the common Laravel frontend, not an unknown
|
|
256
|
+
// one. It used to fail the `&&` above and land in `plain`.
|
|
257
|
+
if (_has_package(composer, ['require', 'require-dev'], _LIVEWIRE_PACKAGE)) {
|
|
258
|
+
return _r('blade-livewire');
|
|
110
259
|
}
|
|
111
260
|
|
|
112
261
|
if (_is_react_shadcn(pkg, components_json)) {
|
|
113
|
-
return
|
|
262
|
+
return _r('react-shadcn');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// React without Radix/shadcn is a served stack, not an unknown one.
|
|
266
|
+
if (_has_package(pkg, _PKG_DEP_KEYS, 'react')) {
|
|
267
|
+
return _r('react');
|
|
114
268
|
}
|
|
115
269
|
|
|
116
270
|
if (_has_vue(pkg)) {
|
|
117
|
-
return
|
|
271
|
+
return _r('vue');
|
|
118
272
|
}
|
|
119
273
|
|
|
120
|
-
|
|
274
|
+
// A recognised-but-unmodelled framework is NOT `plain`. Refusing here is
|
|
275
|
+
// the point: `plain` means "no frontend markers", and conflating the two
|
|
276
|
+
// is what let a Svelte or Inertia project receive Tailwind-only tooling
|
|
277
|
+
// and no warning.
|
|
278
|
+
if (_has_unmodelled_marker(composer, pkg)) {
|
|
279
|
+
return _r(UNSUPPORTED_STACK);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Monorepo: no manifest at the root, but one sits in a workspace directory.
|
|
283
|
+
// That is a wrong-scope call, not a plain project — the caller owns scope
|
|
284
|
+
// selection, so say so instead of labelling the whole repo `plain` and
|
|
285
|
+
// dispatching generic tooling at it.
|
|
286
|
+
//
|
|
287
|
+
// A repo with no manifest ANYWHERE stays `plain`: that is greenfield, and
|
|
288
|
+
// the scaffold path depends on it.
|
|
289
|
+
if (mtime === 0.0) {
|
|
290
|
+
const roots = _nested_frontend_roots(project_root);
|
|
291
|
+
if (roots.length === 1) {
|
|
292
|
+
// Unambiguous scope: descend once and keep the workspace's own
|
|
293
|
+
// axes. Refusing here would punish the common monorepo shape for a
|
|
294
|
+
// scope decision the layout already makes.
|
|
295
|
+
const inner = detect_stack(roots[0] as string);
|
|
296
|
+
return new StackResult({
|
|
297
|
+
frontend: inner.frontend,
|
|
298
|
+
mtime: inner.mtime,
|
|
299
|
+
axes: inner.axes,
|
|
300
|
+
ambiguity: inner.ambiguity,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (roots.length > 1) {
|
|
304
|
+
// Several frontend roots: the caller owns scope selection, so name
|
|
305
|
+
// them and let the step ask. Picking the first would be the silent
|
|
306
|
+
// guess this detector exists to avoid.
|
|
307
|
+
return new StackResult({
|
|
308
|
+
frontend: UNSUPPORTED_STACK,
|
|
309
|
+
mtime,
|
|
310
|
+
axes,
|
|
311
|
+
ambiguity: [
|
|
312
|
+
`workspace roots: ${roots.map((r) => path.basename(r)).join(' + ')}`,
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return _r(DEFAULT_STACK);
|
|
121
319
|
}
|
|
122
320
|
|
|
123
321
|
/**
|
|
@@ -130,9 +328,8 @@ export function detect_stack(project_root: string): StackResult {
|
|
|
130
328
|
* sentinel rather than a missing-file error.
|
|
131
329
|
*/
|
|
132
330
|
export function latest_manifest_mtime(project_root: string): number {
|
|
133
|
-
const candidates = ['composer.json', 'package.json'];
|
|
134
331
|
const mtimes: number[] = [];
|
|
135
|
-
for (const name of
|
|
332
|
+
for (const name of _CACHE_KEY_FILES) {
|
|
136
333
|
const p = path.join(project_root, name);
|
|
137
334
|
if (_is_file(p)) {
|
|
138
335
|
mtimes.push(_stat_mtime(p));
|
|
@@ -141,6 +338,26 @@ export function latest_manifest_mtime(project_root: string): number {
|
|
|
141
338
|
return mtimes.length > 0 ? Math.max(...mtimes) : 0.0;
|
|
142
339
|
}
|
|
143
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Files whose mtime invalidates a cached detection.
|
|
343
|
+
*
|
|
344
|
+
* The two manifests are not sufficient: the signal table also reads marker
|
|
345
|
+
* files, so adding `components.json` or a `nuxt.config.ts` changes the detected
|
|
346
|
+
* axes while leaving both manifests untouched — and the cache would have served
|
|
347
|
+
* the pre-marker answer indefinitely.
|
|
348
|
+
*
|
|
349
|
+
* `components.json` is listed first so a shadcn adoption, the most common of
|
|
350
|
+
* these, is picked up.
|
|
351
|
+
*/
|
|
352
|
+
const _CACHE_KEY_FILES: ReadonlyArray<string> = [
|
|
353
|
+
'composer.json',
|
|
354
|
+
'package.json',
|
|
355
|
+
'components.json',
|
|
356
|
+
'nuxt.config.ts',
|
|
357
|
+
'nuxt.config.js',
|
|
358
|
+
'astro.config.mjs',
|
|
359
|
+
];
|
|
360
|
+
|
|
144
361
|
/**
|
|
145
362
|
* Read a JSON manifest, returning `{}` on any error.
|
|
146
363
|
*
|
|
@@ -185,6 +402,227 @@ function _all_dependencies(manifest: Manifest, ...keys: string[]): Manifest {
|
|
|
185
402
|
return merged;
|
|
186
403
|
}
|
|
187
404
|
|
|
405
|
+
/** Dependency sections a `package.json` can carry. */
|
|
406
|
+
const _PKG_DEP_KEYS: ReadonlyArray<string> = [
|
|
407
|
+
'dependencies',
|
|
408
|
+
'devDependencies',
|
|
409
|
+
'peerDependencies',
|
|
410
|
+
'optionalDependencies',
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
/** True when `name` appears in any of `manifest`'s named dependency sections. */
|
|
414
|
+
function _has_package(
|
|
415
|
+
manifest: Manifest,
|
|
416
|
+
sections: ReadonlyArray<string>,
|
|
417
|
+
name: string,
|
|
418
|
+
): boolean {
|
|
419
|
+
return name in _all_dependencies(manifest, ...sections);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Signal table, one entry per axis value.
|
|
424
|
+
*
|
|
425
|
+
* `first match wins` applies **per axis only** — collapsing that across axes is
|
|
426
|
+
* the original defect. A Nuxt project matches `meta: nuxt` AND
|
|
427
|
+
* `reactivity: vue`; both are true, and the flat label could hold only one.
|
|
428
|
+
*
|
|
429
|
+
* Order within an axis is specificity-descending: a marker file beats a bare
|
|
430
|
+
* dependency, and a wrapper is listed before what it wraps.
|
|
431
|
+
*/
|
|
432
|
+
const _AXIS_SIGNALS: ReadonlyArray<{
|
|
433
|
+
readonly axis: keyof StackAxes;
|
|
434
|
+
readonly value: string;
|
|
435
|
+
readonly npm?: ReadonlyArray<string>;
|
|
436
|
+
readonly npm_prefix?: ReadonlyArray<string>;
|
|
437
|
+
readonly composer?: ReadonlyArray<string>;
|
|
438
|
+
readonly files?: ReadonlyArray<string>;
|
|
439
|
+
}> = [
|
|
440
|
+
// meta — wrappers, checked before the layer they wrap
|
|
441
|
+
{ axis: 'meta', value: 'nextjs', npm: ['next'] },
|
|
442
|
+
{ axis: 'meta', value: 'nuxt', npm: ['nuxt'], files: ['nuxt.config.ts', 'nuxt.config.js'] },
|
|
443
|
+
{ axis: 'meta', value: 'astro', npm: ['astro'], files: ['astro.config.mjs'] },
|
|
444
|
+
{ axis: 'meta', value: 'remix', npm: ['@remix-run/react'] },
|
|
445
|
+
{ axis: 'meta', value: 'sveltekit', npm: ['@sveltejs/kit'] },
|
|
446
|
+
{ axis: 'meta', value: 'filament', composer: ['filament/filament'] },
|
|
447
|
+
|
|
448
|
+
// reactivity
|
|
449
|
+
{ axis: 'reactivity', value: 'react', npm: ['react'] },
|
|
450
|
+
{ axis: 'reactivity', value: 'vue', npm: ['vue'] },
|
|
451
|
+
{ axis: 'reactivity', value: 'svelte', npm: ['svelte'] },
|
|
452
|
+
{ axis: 'reactivity', value: 'angular', npm: ['@angular/core'] },
|
|
453
|
+
{ axis: 'reactivity', value: 'solid', npm: ['solid-js'] },
|
|
454
|
+
{ axis: 'reactivity', value: 'qwik', npm: ['@builder.io/qwik'] },
|
|
455
|
+
{ axis: 'reactivity', value: 'livewire', composer: ['livewire/livewire'] },
|
|
456
|
+
{ axis: 'reactivity', value: 'alpine', npm: ['alpinejs'] },
|
|
457
|
+
{ axis: 'reactivity', value: 'htmx', npm: ['htmx.org'] },
|
|
458
|
+
|
|
459
|
+
// view
|
|
460
|
+
{ axis: 'view', value: 'svelte-sfc', npm: ['svelte'] },
|
|
461
|
+
{ axis: 'view', value: 'vue-sfc', npm: ['vue'] },
|
|
462
|
+
// Angular templates are HTML with its own directive syntax, not JSX.
|
|
463
|
+
// Listing it under `jsx` was wrong and would have handed a React idiom to
|
|
464
|
+
// an Angular project through the Phase-3 composition.
|
|
465
|
+
{ axis: 'view', value: 'angular-html', npm: ['@angular/core'] },
|
|
466
|
+
{ axis: 'view', value: 'astro', npm: ['astro'] },
|
|
467
|
+
{ axis: 'view', value: 'jsx', npm: ['react'] },
|
|
468
|
+
{ axis: 'view', value: 'blade', composer: ['laravel/framework'] },
|
|
469
|
+
|
|
470
|
+
// component_lib — marker file beats dependency
|
|
471
|
+
{ axis: 'component_lib', value: 'shadcn', npm: ['shadcn-ui', 'shadcn'], files: ['components.json'] },
|
|
472
|
+
{ axis: 'component_lib', value: 'radix', npm_prefix: ['@radix-ui/'] },
|
|
473
|
+
{ axis: 'component_lib', value: 'flux', composer: ['livewire/flux'] },
|
|
474
|
+
{ axis: 'component_lib', value: 'nuxt-ui', npm: ['@nuxt/ui'] },
|
|
475
|
+
{ axis: 'component_lib', value: 'vuetify', npm: ['vuetify'] },
|
|
476
|
+
{ axis: 'component_lib', value: 'mui', npm: ['@mui/material'] },
|
|
477
|
+
|
|
478
|
+
// css
|
|
479
|
+
{ axis: 'css', value: 'tailwind', npm: ['tailwindcss'] },
|
|
480
|
+
{ axis: 'css', value: 'bootstrap', npm: ['bootstrap'] },
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
/** Axes where two signals are a genuine conflict rather than a stack. */
|
|
484
|
+
const _AMBIGUOUS_AXES: ReadonlyArray<keyof StackAxes> = ['reactivity'];
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Client-side SPA frameworks — two of these in one manifest is a real conflict.
|
|
488
|
+
*
|
|
489
|
+
* Everything else on the reactivity axis co-exists legitimately: Alpine and htmx
|
|
490
|
+
* are progressive-enhancement layers, and Livewire is server-driven, so
|
|
491
|
+
* Laravel+Livewire with a React widget is an ordinary stack, not an ambiguity.
|
|
492
|
+
* Treating any two reactivity signals as a conflict refused those projects —
|
|
493
|
+
* caught by the existing blade-wins-over-react precedence test.
|
|
494
|
+
*/
|
|
495
|
+
const _EXCLUSIVE_REACTIVITY: ReadonlySet<string> = new Set([
|
|
496
|
+
'react',
|
|
497
|
+
'vue',
|
|
498
|
+
'svelte',
|
|
499
|
+
'angular',
|
|
500
|
+
'solid',
|
|
501
|
+
'qwik',
|
|
502
|
+
]);
|
|
503
|
+
|
|
504
|
+
/** Resolve every axis independently from the manifests plus marker files. */
|
|
505
|
+
function _detect_axes(
|
|
506
|
+
composer: Manifest,
|
|
507
|
+
pkg: Manifest,
|
|
508
|
+
components_json: boolean,
|
|
509
|
+
project_root: string,
|
|
510
|
+
): StackAxes {
|
|
511
|
+
const npm = _all_dependencies(pkg, ..._PKG_DEP_KEYS);
|
|
512
|
+
const php = _all_dependencies(composer, 'require', 'require-dev');
|
|
513
|
+
const npm_names = Object.keys(npm);
|
|
514
|
+
const out: Record<string, string> = { ..._EMPTY_AXES };
|
|
515
|
+
const decided = new Set<string>();
|
|
516
|
+
|
|
517
|
+
for (const sig of _AXIS_SIGNALS) {
|
|
518
|
+
if (decided.has(sig.axis)) continue;
|
|
519
|
+
const hit =
|
|
520
|
+
(sig.npm ?? []).some((n) => n in npm) ||
|
|
521
|
+
(sig.npm_prefix ?? []).some((p) => npm_names.some((n) => n.startsWith(p))) ||
|
|
522
|
+
(sig.composer ?? []).some((n) => n in php) ||
|
|
523
|
+
(sig.files ?? []).some(
|
|
524
|
+
(f) =>
|
|
525
|
+
(f === 'components.json' && components_json) ||
|
|
526
|
+
_is_file(path.join(project_root, f)),
|
|
527
|
+
);
|
|
528
|
+
if (hit) {
|
|
529
|
+
out[sig.axis] = sig.value;
|
|
530
|
+
decided.add(sig.axis);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// An unresolved axis on a project that HAS manifests is "not recognised" —
|
|
535
|
+
// a different fact from "absent", and conflating them is what made a
|
|
536
|
+
// refusal indistinguishable from a genuinely plain project.
|
|
537
|
+
const has_manifest = npm_names.length > 0 || Object.keys(php).length > 0;
|
|
538
|
+
if (has_manifest) {
|
|
539
|
+
for (const axis of ['view', 'reactivity'] as const) {
|
|
540
|
+
if (!decided.has(axis)) out[axis] = 'unknown';
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return out as unknown as StackAxes;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Report axes carrying two mutually exclusive signals. */
|
|
547
|
+
function _detect_ambiguity(composer: Manifest, pkg: Manifest): string[] {
|
|
548
|
+
const npm = _all_dependencies(pkg, ..._PKG_DEP_KEYS);
|
|
549
|
+
const php = _all_dependencies(composer, 'require', 'require-dev');
|
|
550
|
+
const found: string[] = [];
|
|
551
|
+
for (const axis of _AMBIGUOUS_AXES) {
|
|
552
|
+
const hits = _AXIS_SIGNALS.filter(
|
|
553
|
+
(sig) =>
|
|
554
|
+
sig.axis === axis &&
|
|
555
|
+
((sig.npm ?? []).some((n) => n in npm) ||
|
|
556
|
+
(sig.composer ?? []).some((n) => n in php)),
|
|
557
|
+
)
|
|
558
|
+
.map((sig) => sig.value)
|
|
559
|
+
.filter((v) => _EXCLUSIVE_REACTIVITY.has(v));
|
|
560
|
+
if (hits.length > 1) {
|
|
561
|
+
found.push(`${axis}: ${hits.join(' + ')}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return found;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Workspace directories a monorepo conventionally puts its packages in. */
|
|
568
|
+
const _WORKSPACE_DIRS: ReadonlyArray<string> = ['packages', 'apps', 'services', 'libs'];
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Return the workspace roots that carry a manifest, sorted.
|
|
572
|
+
*
|
|
573
|
+
* Reads the declarative `workspaces` field when present and falls back to the
|
|
574
|
+
* conventional directories. Deliberately shallow — one `readdir` per candidate
|
|
575
|
+
* directory, one `stat` per child.
|
|
576
|
+
*
|
|
577
|
+
* Empty means "not a monorepo". Exactly one means the scope is unambiguous and
|
|
578
|
+
* detection can descend into it. More than one is a question for the caller,
|
|
579
|
+
* not a guess for the detector.
|
|
580
|
+
*/
|
|
581
|
+
function _nested_frontend_roots(project_root: string): string[] {
|
|
582
|
+
const roots: string[] = [];
|
|
583
|
+
const seen = new Set<string>();
|
|
584
|
+
// `workspaces` is the declarative answer when present; the conventional
|
|
585
|
+
// directories are the fallback for repos that do not declare them.
|
|
586
|
+
const root_pkg = _read_json(path.join(project_root, 'package.json'));
|
|
587
|
+
const declared = root_pkg['workspaces'];
|
|
588
|
+
const globs: string[] = Array.isArray(declared)
|
|
589
|
+
? (declared as unknown[]).filter((g): g is string => typeof g === 'string')
|
|
590
|
+
: [];
|
|
591
|
+
const dirs = new Set<string>(_WORKSPACE_DIRS);
|
|
592
|
+
for (const g of globs) {
|
|
593
|
+
const head = g.split('/')[0];
|
|
594
|
+
if (head !== undefined && head !== '' && !head.includes('*')) dirs.add(head);
|
|
595
|
+
}
|
|
596
|
+
for (const dir of dirs) {
|
|
597
|
+
const base = path.join(project_root, dir);
|
|
598
|
+
let entries: string[];
|
|
599
|
+
try {
|
|
600
|
+
entries = fs.readdirSync(base);
|
|
601
|
+
} catch {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
for (const entry of entries) {
|
|
605
|
+
const child = path.join(base, entry);
|
|
606
|
+
if (seen.has(child)) continue;
|
|
607
|
+
if (
|
|
608
|
+
_is_file(path.join(child, 'package.json')) ||
|
|
609
|
+
_is_file(path.join(child, 'composer.json'))
|
|
610
|
+
) {
|
|
611
|
+
seen.add(child);
|
|
612
|
+
roots.push(child);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return roots.sort();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** True when either manifest names a framework we recognise but do not model. */
|
|
620
|
+
function _has_unmodelled_marker(composer: Manifest, pkg: Manifest): boolean {
|
|
621
|
+
const composer_deps = _all_dependencies(composer, 'require', 'require-dev');
|
|
622
|
+
const pkg_deps = _all_dependencies(pkg, ..._PKG_DEP_KEYS);
|
|
623
|
+
return _UNMODELLED_MARKERS.some((marker) => marker in composer_deps || marker in pkg_deps);
|
|
624
|
+
}
|
|
625
|
+
|
|
188
626
|
function _is_blade_livewire_flux(composer: Manifest): boolean {
|
|
189
627
|
const deps = _all_dependencies(composer, 'require', 'require-dev');
|
|
190
628
|
return _LIVEWIRE_PACKAGE in deps && _FLUX_PACKAGE in deps;
|
|
@@ -629,10 +629,12 @@ function _validate_app_spec(app_spec: JsonValue): void {
|
|
|
629
629
|
* agent-directive that populates it. An empty dict is the in-progress
|
|
630
630
|
* shape after the skill returns but before the brief lands; the gate
|
|
631
631
|
* treats it the same as `null`. Once populated, `design_confirmed`
|
|
632
|
-
* (when present) must be a bool
|
|
633
|
-
* `
|
|
634
|
-
*
|
|
635
|
-
*
|
|
632
|
+
* (when present) must be a bool, and `states` (when present and not
|
|
633
|
+
* `null`) must be an object — the design gate's per-state loop is
|
|
634
|
+
* object-guarded, so a string or list would bypass all five checks.
|
|
635
|
+
* The remaining keys (`layout`, `components`, `microcopy`, `a11y`,
|
|
636
|
+
* `reused_from_audit`) are validated by the design handler against the
|
|
637
|
+
* skill contract — the schema only enforces shape, not content.
|
|
636
638
|
*/
|
|
637
639
|
function _validate_ui_design(ui_design: JsonValue): void {
|
|
638
640
|
if (ui_design === null) {
|
|
@@ -652,6 +654,21 @@ function _validate_ui_design(ui_design: JsonValue): void {
|
|
|
652
654
|
'state.ui_design.design_confirmed must be a boolean when present',
|
|
653
655
|
);
|
|
654
656
|
}
|
|
657
|
+
// `states` is shape-checked here, not left to the handler. The design
|
|
658
|
+
// gate's per-state loop was guarded on "is this an object", so a string or
|
|
659
|
+
// list silently skipped all five required-state checks — the gate passed
|
|
660
|
+
// while covering no state at all. Rejecting the wrong shape at the schema
|
|
661
|
+
// boundary means the loop can no longer be bypassed by type.
|
|
662
|
+
if (
|
|
663
|
+
'states' in ui_design &&
|
|
664
|
+
ui_design['states'] !== null &&
|
|
665
|
+
!_isDict(ui_design['states'])
|
|
666
|
+
) {
|
|
667
|
+
throw new SchemaError(
|
|
668
|
+
'state.ui_design.states must be a JSON object (one key per ' +
|
|
669
|
+
`required state) or null; got ${pyTypeName(ui_design['states'])}`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
655
672
|
}
|
|
656
673
|
|
|
657
674
|
/**
|
|
@@ -62,6 +62,11 @@
|
|
|
62
62
|
description: Laravel framework patterns; depends on PHP at the artefact level.
|
|
63
63
|
workspaces: [engineering]
|
|
64
64
|
requires: [php, engineering-base]
|
|
65
|
+
# The design layer is a companion, not a dependency: this pack ships UI
|
|
66
|
+
# executors (blade-ui, flux, livewire) whose grounded selection lives in
|
|
67
|
+
# frontend-design, but a Laravel API-only project needs none of it. Advisory
|
|
68
|
+
# so the wizard offers it; `requires` would force the weight on every install.
|
|
69
|
+
suggests: [frontend-design]
|
|
65
70
|
cluster: php
|
|
66
71
|
trust_level_default: professional
|
|
67
72
|
domain: engineering
|
|
@@ -98,6 +103,8 @@
|
|
|
98
103
|
description: React framework patterns.
|
|
99
104
|
workspaces: [engineering]
|
|
100
105
|
requires: [javascript, engineering-base]
|
|
106
|
+
# Companion, not dependency — see the note on `laravel`.
|
|
107
|
+
suggests: [frontend-design]
|
|
101
108
|
cluster: typescript
|
|
102
109
|
trust_level_default: professional
|
|
103
110
|
domain: engineering
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"unpacked_size_mb": {
|
|
9
9
|
"max": 28,
|
|
10
10
|
"last_measured": 26.05,
|
|
11
|
-
"method": "npm pack --json → dist.unpackedSize / 1e6 — includes the Phase-1 precompiled bundles (dist/hooks/dispatch.js + dist/mcp/server.mjs, ~2.1 MB) that bought the 23x hook-latency win and the tsx removal"
|
|
11
|
+
"method": "npm pack --json → dist.unpackedSize / 1e6 — includes the Phase-1 precompiled bundles (dist/hooks/dispatch.js + dist/mcp/server.mjs, ~2.1 MB) that bought the 23x hook-latency win and the tsx removal, plus dist/cli-delegate/ (~1.40 MB, ADR-204) which finished that removal for the consumer command surface. This gate is the ONLY size trip-wire on those bundles by design: a dedicated per-bundle budget key would need its own measurement wired into evaluator_umbrella.sh, and an unwired key fails check_evaluator_budgets with 'no measurement supplied' instead of guarding anything. ADR-204 projected 27.45 against max 28 without packing — the next real measurement here is the confirming one, and headroom is thin"
|
|
12
12
|
},
|
|
13
13
|
"node_modules_mb": {
|
|
14
14
|
"max": 140,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Payload budget for the published npm tarball (road-to-zero-ceremony-install Phase 4). MEASURED, NEVER INVENTED — the numbers below come from `npm pack --dry-run --json --ignore-scripts` on the tracked tree at 9.9.0 (2026-07-31). Two metrics, both genuinely ungated before this file: COMPRESSED tarball size, and PER-SKILL share of the skills subtree. The UNPACKED size is deliberately absent — it is already owned by src/config/evaluator-budgets.json (unpacked_size_mb), and a second number for one lever is how three conflicting figures start circulating.",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"owner": "maintainer",
|
|
5
|
+
"review_by": "2027-07-31",
|
|
6
|
+
"regression_pct": 10,
|
|
7
|
+
"measurement_conditions": "npm pack --dry-run --json --ignore-scripts, run at the repo root on a clean checkout. `--ignore-scripts` means NO build runs, so dist/cli, dist/ui, dist/mcp and dist/hooks are absent and the figure covers only the TRACKED payload (src/scripts/, dist/agent-src/, dist/install/, docs/guidelines/, templates). That is deliberate: this gate is always-on for every PR, and the tracked content tree is exactly the surface that grows by accretion. The built artifact is ~5.4 MB larger and is gated separately, at release time, by evaluator-budgets.unpacked_size_mb.",
|
|
8
|
+
"budgets": {
|
|
9
|
+
"packed_size_mb": {
|
|
10
|
+
"max": 6.4,
|
|
11
|
+
"last_measured": 5.903,
|
|
12
|
+
"method": "npm pack --dry-run --json --ignore-scripts → dist.size / 1e6 (the compressed tarball a consumer actually downloads)"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"per_skill_share": {
|
|
16
|
+
"max_pct": 5.0,
|
|
17
|
+
"basis": "a single skill's share of the total bytes under dist/agent-src/skills/ in the tarball",
|
|
18
|
+
"rationale": "Measured 2026-07-31: the largest non-excepted skill is corpus-grounding at 2.33%, so a 5% cap is green with better than 2x headroom and red for any skill that doubles. It is a falsifiable gate, not a ritual one.",
|
|
19
|
+
"exceptions": {
|
|
20
|
+
"design-intelligence": {
|
|
21
|
+
"max_pct": 23.0,
|
|
22
|
+
"measured_pct": 22.63,
|
|
23
|
+
"reason": "771 KB of BM25 corpus (27 CSVs + 16 design-language prose specs + manifest.json) consumed at runtime by corpus-grounding's retriever, and referenced by 6 further skills. Moving it behind a lazy fetch would add a network dependency to a skill that works offline today — the same trade-off ADR-061 §8 already REJECTED for a 745 KB font index. The exception is visible debt, not an endorsement: raising max_pct requires a reason in the same commit."
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* the Phase-1b rule filter, so scoped installs get scoped host files
|
|
22
22
|
* for free;
|
|
23
23
|
* - re-using the exported emitters keeps consumer output byte-identical
|
|
24
|
-
* to the maintainer projection (
|
|
24
|
+
* to the maintainer projection (the consumer install never drifts from
|
|
25
|
+
* Pipeline A, the source projection into `dist/agent-src/`).
|
|
25
26
|
* The trade-off (node required at install time) matches the rule-scope
|
|
26
27
|
* resolver: no node → install.sh keeps its legacy fallback surfaces.
|
|
27
28
|
*
|