@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
|
@@ -146,7 +146,17 @@ function _missing_required_keys(design: Record<string, Any>): string[] {
|
|
|
146
146
|
missing.push(key);
|
|
147
147
|
continue;
|
|
148
148
|
}
|
|
149
|
-
if (key === 'states'
|
|
149
|
+
if (key === 'states') {
|
|
150
|
+
// A non-object `states` (a string, a list) used to skip the
|
|
151
|
+
// per-state loop entirely, so `states: "n/a"` satisfied the gate
|
|
152
|
+
// without covering a single state. Report every required key
|
|
153
|
+
// instead — the author gets the same actionable list either way.
|
|
154
|
+
if (!_isDict(value)) {
|
|
155
|
+
for (const state_key of REQUIRED_STATE_KEYS) {
|
|
156
|
+
missing.push(`states.${state_key}`);
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
150
160
|
for (const state_key of REQUIRED_STATE_KEYS) {
|
|
151
161
|
if (!_pyTruthy(value[state_key])) {
|
|
152
162
|
missing.push(`states.${state_key}`);
|
|
@@ -157,37 +167,74 @@ function _missing_required_keys(design: Record<string, Any>): string[] {
|
|
|
157
167
|
return missing;
|
|
158
168
|
}
|
|
159
169
|
|
|
160
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Return brief paths whose values match a placeholder pattern.
|
|
172
|
+
*
|
|
173
|
+
* Covers `microcopy` **and** `states`. The five required states were checked
|
|
174
|
+
* for truthiness only, so `states.error: "TBD"` satisfied the gate — the brief
|
|
175
|
+
* looked complete while covering nothing. Both slots carry strings the user
|
|
176
|
+
* signs off on, so both are held to the same bar.
|
|
177
|
+
*
|
|
178
|
+
* An explicit `"n/a"` stays legitimate: a static landing page genuinely has no
|
|
179
|
+
* error or disabled state, and *declaring* that is the opposite of inventing
|
|
180
|
+
* filler. That is why the gate demands all five keys rather than branching on
|
|
181
|
+
* page type — the author states the answer instead of the engine guessing which
|
|
182
|
+
* states a surface ought to have.
|
|
183
|
+
*/
|
|
161
184
|
function _placeholder_violations(design: Record<string, Any>): string[] {
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
return [
|
|
186
|
+
...placeholder_paths(design['microcopy'], 'microcopy'),
|
|
187
|
+
...placeholder_paths(design['states'], 'states'),
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Return every path under `node` whose leaf string carries a placeholder.
|
|
193
|
+
*
|
|
194
|
+
* Traverses objects **and arrays**. The array arm is load-bearing, not
|
|
195
|
+
* defensive: a list is the natural shape for the most common microcopy
|
|
196
|
+
* (`nav_items`, `menu`, `steps`) and for a rendered file split into lines,
|
|
197
|
+
* and a walker that recursed into objects only let
|
|
198
|
+
* `{ nav_items: ["Home", "TODO: Link"] }` through both the brief lock and
|
|
199
|
+
* the rendered-output gate.
|
|
200
|
+
*
|
|
201
|
+
* Array elements are addressed `key[i]` so the halt names the exact element
|
|
202
|
+
* the author has to fix, not just the list.
|
|
203
|
+
*
|
|
204
|
+
* Shared by `design` (producer side, on `microcopy`) and `apply` (consumer
|
|
205
|
+
* side, on the rendered envelope). One implementation on purpose: the two
|
|
206
|
+
* call sites previously carried byte-identical copies of the same bug, so
|
|
207
|
+
* duplicating them bought no independence — see
|
|
208
|
+
* `docs/contracts/ui-track-flow.md` § implement → apply.
|
|
209
|
+
*/
|
|
210
|
+
export function placeholder_paths(node: Any, prefix = ''): string[] {
|
|
164
211
|
const violations: string[] = [];
|
|
165
|
-
|
|
212
|
+
_walk_placeholders(node, prefix, violations);
|
|
166
213
|
return violations;
|
|
167
214
|
}
|
|
168
215
|
|
|
169
|
-
function
|
|
170
|
-
node
|
|
171
|
-
|
|
172
|
-
violations: string[],
|
|
173
|
-
): void {
|
|
174
|
-
for (const key of Object.keys(node)) {
|
|
175
|
-
const value = node[key];
|
|
176
|
-
const path = prefix ? `${prefix}.${key}` : String(key);
|
|
177
|
-
if (_isDict(value)) {
|
|
178
|
-
_walk_microcopy(value, path, violations);
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (typeof value !== 'string') {
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const lowered = value.toLowerCase();
|
|
216
|
+
function _walk_placeholders(node: Any, prefix: string, violations: string[]): void {
|
|
217
|
+
if (typeof node === 'string') {
|
|
218
|
+
const lowered = node.toLowerCase();
|
|
185
219
|
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
186
220
|
if (lowered.includes(pattern)) {
|
|
187
|
-
violations.push(
|
|
188
|
-
|
|
221
|
+
violations.push(prefix);
|
|
222
|
+
return;
|
|
189
223
|
}
|
|
190
224
|
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(node)) {
|
|
228
|
+
node.forEach((value, index) => {
|
|
229
|
+
_walk_placeholders(value, `${prefix}[${index}]`, violations);
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (_isDict(node)) {
|
|
234
|
+
for (const key of Object.keys(node)) {
|
|
235
|
+
const path = prefix ? `${prefix}.${key}` : String(key);
|
|
236
|
+
_walk_placeholders(node[key], path, violations);
|
|
237
|
+
}
|
|
191
238
|
}
|
|
192
239
|
}
|
|
193
240
|
|
|
@@ -311,15 +358,23 @@ function _halt_unconfirmed(state: DeliveryState, design: Record<string, Any>): S
|
|
|
311
358
|
});
|
|
312
359
|
}
|
|
313
360
|
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Count leaf string entries in `microcopy` (recursive, arrays included).
|
|
363
|
+
*
|
|
364
|
+
* The count is shown in the sign-off the user confirms, so it has to agree
|
|
365
|
+
* with what the placeholder walker inspects — an object-only count reported
|
|
366
|
+
* fewer locked strings than were actually locked.
|
|
367
|
+
*/
|
|
368
|
+
function _count_microcopy(microcopy: Any): number {
|
|
369
|
+
if (typeof microcopy === 'string') return 1;
|
|
370
|
+
if (Array.isArray(microcopy)) {
|
|
371
|
+
return microcopy.reduce((total: number, value) => total + _count_microcopy(value), 0);
|
|
372
|
+
}
|
|
373
|
+
if (_isDict(microcopy)) {
|
|
374
|
+
return Object.values(microcopy).reduce(
|
|
375
|
+
(total: number, value) => total + _count_microcopy(value),
|
|
376
|
+
0,
|
|
377
|
+
);
|
|
323
378
|
}
|
|
324
|
-
return
|
|
379
|
+
return 0;
|
|
325
380
|
}
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
StepResult,
|
|
16
16
|
agent_directive,
|
|
17
17
|
} from '../../delivery_state.js';
|
|
18
|
+
import {
|
|
19
|
+
is_ambiguous_stack,
|
|
20
|
+
bundle_line,
|
|
21
|
+
unsupported_stack_questions,
|
|
22
|
+
} from './stack_bundles.js';
|
|
18
23
|
|
|
19
24
|
/** Maximum number of polish rounds per `/work` run. */
|
|
20
25
|
export const POLISH_CEILING = 2;
|
|
@@ -31,9 +36,15 @@ export const TOKEN_REPEAT_THRESHOLD = 2;
|
|
|
31
36
|
/** Map `state.stack.frontend` → agent-directive skill name. */
|
|
32
37
|
export const STACK_DIRECTIVES: Record<string, string> = {
|
|
33
38
|
'blade-livewire-flux': 'ui-polish-blade-livewire-flux',
|
|
39
|
+
'blade-livewire': 'ui-polish-blade-livewire',
|
|
40
|
+
filament: 'ui-polish-filament',
|
|
34
41
|
'react-shadcn': 'ui-polish-react-shadcn',
|
|
42
|
+
react: 'ui-polish-react',
|
|
35
43
|
vue: 'ui-polish-vue',
|
|
36
44
|
plain: 'ui-polish-plain',
|
|
45
|
+
// Present so the `keys == KNOWN_STACKS` invariant holds. Never emitted —
|
|
46
|
+
// the step intercepts this lane and refuses instead of dispatching.
|
|
47
|
+
unknown: 'ui-polish-unsupported',
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
/** Fallback directive when `state.stack` is missing or malformed. */
|
|
@@ -184,6 +195,17 @@ function _delegate_to_polish_skill(
|
|
|
184
195
|
ceiling: number,
|
|
185
196
|
): StepResult {
|
|
186
197
|
const directive = _resolve_directive(state);
|
|
198
|
+
if (is_ambiguous_stack(state.stack)) {
|
|
199
|
+
const conflicts = ((state.stack as Record<string, Any>)['ambiguity'] ??
|
|
200
|
+
[]) as ReadonlyArray<string>;
|
|
201
|
+
return new StepResult({
|
|
202
|
+
outcome: Outcome.BLOCKED,
|
|
203
|
+
questions: unsupported_stack_questions('polish', conflicts),
|
|
204
|
+
message:
|
|
205
|
+
`UI polish halted: detection is ambiguous (${conflicts.join('; ')}) — ` +
|
|
206
|
+
'the open question is which project to build for.',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
187
209
|
const stack_label = _stack_label(state);
|
|
188
210
|
const next_round = rounds + 1;
|
|
189
211
|
let findings_line =
|
|
@@ -203,6 +225,7 @@ function _delegate_to_polish_skill(
|
|
|
203
225
|
questions: [
|
|
204
226
|
agent_directive(directive),
|
|
205
227
|
`> Stack: \`${stack_label}\`. Polish round ${next_round} of ${ceiling}.`,
|
|
228
|
+
bundle_line(state.stack, 'build', stack_label),
|
|
206
229
|
findings_line,
|
|
207
230
|
'> Fix chart-type / contrast findings against the adopted ' +
|
|
208
231
|
"corpus rows (design-intelligence § 'Grounding the " +
|
|
@@ -16,13 +16,24 @@ import {
|
|
|
16
16
|
StepResult,
|
|
17
17
|
agent_directive,
|
|
18
18
|
} from '../../delivery_state.js';
|
|
19
|
+
import {
|
|
20
|
+
is_ambiguous_stack,
|
|
21
|
+
bundle_line,
|
|
22
|
+
unsupported_stack_questions,
|
|
23
|
+
} from './stack_bundles.js';
|
|
19
24
|
|
|
20
|
-
/** Map `state.stack.frontend` → agent-directive
|
|
25
|
+
/** Map `state.stack.frontend` → agent-directive verb. */
|
|
21
26
|
export const STACK_DIRECTIVES: Record<string, string> = {
|
|
22
27
|
'blade-livewire-flux': 'ui-design-review-blade-livewire-flux',
|
|
28
|
+
'blade-livewire': 'ui-design-review-blade-livewire',
|
|
29
|
+
filament: 'ui-design-review-filament',
|
|
23
30
|
'react-shadcn': 'ui-design-review-react-shadcn',
|
|
31
|
+
react: 'ui-design-review-react',
|
|
24
32
|
vue: 'ui-design-review-vue',
|
|
25
33
|
plain: 'ui-design-review-plain',
|
|
34
|
+
// Present so the `keys == KNOWN_STACKS` invariant holds. Never emitted —
|
|
35
|
+
// the step intercepts this lane and refuses instead of dispatching.
|
|
36
|
+
unknown: 'ui-design-review-unsupported',
|
|
26
37
|
};
|
|
27
38
|
|
|
28
39
|
/** Fallback directive when `state.stack` is missing or malformed. */
|
|
@@ -181,12 +192,24 @@ function _stack_label(state: DeliveryState): string {
|
|
|
181
192
|
function _delegate_to_review_skill(state: DeliveryState): StepResult {
|
|
182
193
|
const directive = _resolve_directive(state);
|
|
183
194
|
const stack_label = _stack_label(state);
|
|
195
|
+
if (is_ambiguous_stack(state.stack)) {
|
|
196
|
+
const conflicts = ((state.stack as Record<string, Any>)['ambiguity'] ??
|
|
197
|
+
[]) as ReadonlyArray<string>;
|
|
198
|
+
return new StepResult({
|
|
199
|
+
outcome: Outcome.BLOCKED,
|
|
200
|
+
questions: unsupported_stack_questions('review', conflicts),
|
|
201
|
+
message:
|
|
202
|
+
`UI review halted: detection is ambiguous (${conflicts.join('; ')}) — ` +
|
|
203
|
+
'the open question is which project to build for.',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
184
206
|
return new StepResult({
|
|
185
207
|
outcome: Outcome.BLOCKED,
|
|
186
208
|
questions: [
|
|
187
209
|
agent_directive(directive),
|
|
188
210
|
`> Stack: \`${stack_label}\`. Reviewing rendered components ` +
|
|
189
211
|
'against the locked design brief.',
|
|
212
|
+
bundle_line(state.stack, 'review', stack_label),
|
|
190
213
|
'> The review pass compares `state.ticket.ui_apply.rendered` ' +
|
|
191
214
|
'against `state.ui_design` (microcopy, states, a11y, layout) ' +
|
|
192
215
|
'and produces a structured `findings` list.',
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verb → skill-bundle map for the stack-dispatched UI steps.
|
|
3
|
+
*
|
|
4
|
+
* The UI steps halt with `@agent-directive: ui-apply-<stack>` (and the
|
|
5
|
+
* `ui-design-review-` / `ui-polish-` equivalents). Those names are **directive
|
|
6
|
+
* verbs, not skill paths** — the same shape as `run-tests`, `create-plan`, and
|
|
7
|
+
* `apply-plan`, which likewise resolve to no skill file. Only 2 of the engine's
|
|
8
|
+
* 11 literal verbs name a real skill, so a verb is something the agent
|
|
9
|
+
* interprets, and the mapping from verb to the skills that implement it is a
|
|
10
|
+
* separate fact that has to be written down somewhere.
|
|
11
|
+
*
|
|
12
|
+
* Before this module that mapping lived only in a prose table in
|
|
13
|
+
* `docs/contracts/ui-track-flow.md`. Nothing verified it, and two of its four
|
|
14
|
+
* rows were wrong: `vue` pointed at the directive verb itself (naming no skill
|
|
15
|
+
* at all) and `plain` — which is also the fallback for every unrecognised
|
|
16
|
+
* project — pointed at `blade-ui`, a skill whose frontmatter is
|
|
17
|
+
* `packs: [laravel]` and which a non-Laravel consumer therefore does not have
|
|
18
|
+
* installed.
|
|
19
|
+
*
|
|
20
|
+
* Keeping the map here makes it checkable: `lint_ui_stack_bundles` asserts every
|
|
21
|
+
* member resolves to a real skill, and that no lane reachable without the
|
|
22
|
+
* `laravel` pack names a `laravel`-only skill. A CI check in this repo cannot
|
|
23
|
+
* see a consumer's installed pack combination, so the second assertion is the
|
|
24
|
+
* part that actually prevents the `plain → blade-ui` class of defect.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Skills that implement a lane, in the order the agent should load them. */
|
|
28
|
+
export interface StackBundle {
|
|
29
|
+
/** Skills that build / fix UI for this lane (`apply`, `polish`). */
|
|
30
|
+
readonly build: ReadonlyArray<string>;
|
|
31
|
+
/** Skills that review UI for this lane (`review`). */
|
|
32
|
+
readonly review: ReadonlyArray<string>;
|
|
33
|
+
/**
|
|
34
|
+
* True when every skill above ships outside the framework packs, i.e. the
|
|
35
|
+
* lane is safe as a fallback for a consumer whose packs we cannot predict.
|
|
36
|
+
* Only lanes marked `pack_agnostic` may be used as `DEFAULT_*`.
|
|
37
|
+
*/
|
|
38
|
+
readonly pack_agnostic: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The stack-neutral build bundle. All three live in `engineering-base`, so they
|
|
43
|
+
* are present in every install.
|
|
44
|
+
*
|
|
45
|
+
* `ui-apply-generic` leads: it carries the implementation contract that used to
|
|
46
|
+
* exist only inside the framework executors, and it pulls framework idiom from
|
|
47
|
+
* the 16-stack corpus rather than from prose. That is what turns this lane from
|
|
48
|
+
* an honest refusal into actual coverage for Svelte, Astro, Angular and
|
|
49
|
+
* anything future — without minting a skill per framework.
|
|
50
|
+
*
|
|
51
|
+
* The other two are the companions it composes with: `ui-component-architect`
|
|
52
|
+
* ("the same lens applies to Blade, Livewire, React, or Vue trees") and
|
|
53
|
+
* `tailwind-engineer` ("the **how** for any Tailwind-stack screen").
|
|
54
|
+
*/
|
|
55
|
+
const GENERIC_BUILD: ReadonlyArray<string> = [
|
|
56
|
+
'ui-apply-generic',
|
|
57
|
+
'ui-component-architect',
|
|
58
|
+
'tailwind-engineer',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** `design-review` is stack-neutral and ships in `engineering-base`. */
|
|
62
|
+
const GENERIC_REVIEW: ReadonlyArray<string> = ['design-review'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lane → bundle. Keys mirror `KNOWN_STACKS`; the dispatch tables in
|
|
66
|
+
* `apply` / `review` / `polish` keep their own verb strings (a tested public
|
|
67
|
+
* contract) and consume this map only to name the skills in the halt body.
|
|
68
|
+
*/
|
|
69
|
+
export const STACK_BUNDLES: Readonly<Record<string, StackBundle>> = {
|
|
70
|
+
// An unmodelled framework routes to the generic lane rather than refusing:
|
|
71
|
+
// `ui-apply-generic` queries the stack corpus, which covers svelte, astro,
|
|
72
|
+
// angular, nuxt and eleven more. Refusing while holding that corpus is the
|
|
73
|
+
// "an honest refusal is not coverage" defect. A genuine ambiguity still
|
|
74
|
+
// refuses — see `is_ambiguous_stack`.
|
|
75
|
+
unknown: {
|
|
76
|
+
build: GENERIC_BUILD,
|
|
77
|
+
review: GENERIC_REVIEW,
|
|
78
|
+
pack_agnostic: true,
|
|
79
|
+
},
|
|
80
|
+
'blade-livewire-flux': {
|
|
81
|
+
build: ['flux', 'livewire', 'blade-ui'],
|
|
82
|
+
review: GENERIC_REVIEW,
|
|
83
|
+
pack_agnostic: false,
|
|
84
|
+
},
|
|
85
|
+
// Livewire without Flux — the common Laravel frontend. Previously fell
|
|
86
|
+
// into `plain` because detection required Livewire AND Flux.
|
|
87
|
+
'blade-livewire': {
|
|
88
|
+
build: ['livewire', 'blade-ui'],
|
|
89
|
+
review: GENERIC_REVIEW,
|
|
90
|
+
pack_agnostic: false,
|
|
91
|
+
},
|
|
92
|
+
// Filament is Livewire + Blade + Tailwind under the hood; the same
|
|
93
|
+
// executors apply. A Filament-specific skill is gated on evidence that
|
|
94
|
+
// these produce unusable output for it, not assumed.
|
|
95
|
+
filament: {
|
|
96
|
+
build: ['livewire', 'blade-ui'],
|
|
97
|
+
review: GENERIC_REVIEW,
|
|
98
|
+
pack_agnostic: false,
|
|
99
|
+
},
|
|
100
|
+
'react-shadcn': {
|
|
101
|
+
build: ['react-shadcn-ui'],
|
|
102
|
+
review: GENERIC_REVIEW,
|
|
103
|
+
pack_agnostic: false,
|
|
104
|
+
},
|
|
105
|
+
// React without Radix/shadcn. `react-shadcn-ui` still carries the React
|
|
106
|
+
// component idiom; the shadcn-specific parts simply do not apply.
|
|
107
|
+
react: {
|
|
108
|
+
build: ['react-shadcn-ui'],
|
|
109
|
+
review: GENERIC_REVIEW,
|
|
110
|
+
pack_agnostic: false,
|
|
111
|
+
},
|
|
112
|
+
// No Vue executor exists. The previous prose row pointed at the directive
|
|
113
|
+
// verb itself, which named nothing; the generic pair is a smaller lie and a
|
|
114
|
+
// stated one — the halt body says the lane has no Vue-specific skill.
|
|
115
|
+
vue: {
|
|
116
|
+
build: GENERIC_BUILD,
|
|
117
|
+
review: GENERIC_REVIEW,
|
|
118
|
+
pack_agnostic: true,
|
|
119
|
+
},
|
|
120
|
+
plain: {
|
|
121
|
+
build: GENERIC_BUILD,
|
|
122
|
+
review: GENERIC_REVIEW,
|
|
123
|
+
pack_agnostic: true,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Lanes served by the stack-neutral pair rather than a framework executor.
|
|
129
|
+
*
|
|
130
|
+
* `plain` and `vue` share the bundle but not the meaning, and the halt body has
|
|
131
|
+
* to say which it is. For `plain` the pair IS the right answer — there is no
|
|
132
|
+
* framework to be idiomatic about. For `vue` it is a gap: a Vue project has a
|
|
133
|
+
* definite idiom and the package has no executor for it, so the result must not
|
|
134
|
+
* read as Vue support.
|
|
135
|
+
*/
|
|
136
|
+
export const GENERIC_LANES: ReadonlySet<string> = new Set(['vue', 'plain', 'unknown']);
|
|
137
|
+
|
|
138
|
+
/** Lanes where the stack-neutral pair is the correct answer, not a shortfall. */
|
|
139
|
+
const NATIVELY_GENERIC_LANES: ReadonlySet<string> = new Set(['plain']);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The label detection returns for a framework it recognises but does not
|
|
143
|
+
* model. Mirrors `stack/detect.ts::UNSUPPORTED_STACK`; duplicated as a literal
|
|
144
|
+
* because this module is import-free by design (the detector is a leaf module
|
|
145
|
+
* the directives must not depend on).
|
|
146
|
+
*/
|
|
147
|
+
export const UNSUPPORTED_LANE = 'unknown';
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Return the bundle for a stack state.
|
|
151
|
+
*
|
|
152
|
+
* Composition wins when the state carries axes; the hand-written map is the
|
|
153
|
+
* fallback for a legacy `state.stack` persisted before Detection v2. Keeping
|
|
154
|
+
* both is not indecision — a state file written by an older run must still
|
|
155
|
+
* dispatch, and the map is exactly what it expects.
|
|
156
|
+
*/
|
|
157
|
+
export function bundle_for(stack_state: unknown): StackBundle {
|
|
158
|
+
if (typeof stack_state === 'object' && stack_state !== null) {
|
|
159
|
+
const raw = (stack_state as Record<string, unknown>)['axes'];
|
|
160
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
161
|
+
return compose_bundle(raw as Record<string, string>);
|
|
162
|
+
}
|
|
163
|
+
const label = (stack_state as Record<string, unknown>)['frontend'];
|
|
164
|
+
if (typeof label === 'string' && label in STACK_BUNDLES) {
|
|
165
|
+
return STACK_BUNDLES[label] as StackBundle;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (typeof stack_state === 'string' && stack_state in STACK_BUNDLES) {
|
|
169
|
+
return STACK_BUNDLES[stack_state] as StackBundle;
|
|
170
|
+
}
|
|
171
|
+
return STACK_BUNDLES['plain'] as StackBundle;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* True when the `unknown` label came from a question, not from a coverage gap.
|
|
176
|
+
*
|
|
177
|
+
* `unknown` carries two different situations and they need opposite handling:
|
|
178
|
+
*
|
|
179
|
+
* - **An unmodelled framework** (svelte, astro, angular …). Since
|
|
180
|
+
* `ui-apply-generic` exists and queries the stack corpus, refusing here would
|
|
181
|
+
* be the "an honest refusal is not coverage" defect — the corpus almost
|
|
182
|
+
* certainly HAS the stack. Route to the generic lane, naming the framework.
|
|
183
|
+
* - **A genuine ambiguity** — two SPA frameworks in one manifest, or several
|
|
184
|
+
* workspace roots. No executor can fix that: the open question is *which
|
|
185
|
+
* project is this*, and only the caller can answer. Refuse.
|
|
186
|
+
*
|
|
187
|
+
* `StackResult.ambiguity` is what distinguishes them, so the check is on the
|
|
188
|
+
* ambiguity list rather than on the label.
|
|
189
|
+
*/
|
|
190
|
+
export function is_ambiguous_stack(stack_state: unknown): boolean {
|
|
191
|
+
if (typeof stack_state !== 'object' || stack_state === null) return false;
|
|
192
|
+
const raw = (stack_state as Record<string, unknown>)['ambiguity'];
|
|
193
|
+
return Array.isArray(raw) && raw.length > 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Questions for the refusal halt when detection cannot say what the project is.
|
|
198
|
+
*
|
|
199
|
+
* Not a directive — there is nothing honest to dispatch to, because the missing
|
|
200
|
+
* input is a decision rather than a skill. `conflicts` are echoed verbatim from
|
|
201
|
+
* `StackResult.ambiguity` so the user sees the actual collision.
|
|
202
|
+
*/
|
|
203
|
+
export function unsupported_stack_questions(
|
|
204
|
+
step: string,
|
|
205
|
+
conflicts: ReadonlyArray<string> = [],
|
|
206
|
+
): string[] {
|
|
207
|
+
const lines = [
|
|
208
|
+
'> Detection found conflicting signals, so it will not pick a stack for ' +
|
|
209
|
+
`you at \`${step}\`. Guessing here is the failure this halt exists ` +
|
|
210
|
+
'to prevent.',
|
|
211
|
+
];
|
|
212
|
+
for (const c of conflicts) {
|
|
213
|
+
lines.push(`> - \`${c}\``);
|
|
214
|
+
}
|
|
215
|
+
lines.push(
|
|
216
|
+
'> 1. Name the project to build for — the workspace path, or which ' +
|
|
217
|
+
'framework owns this surface. Detection re-runs against that scope.',
|
|
218
|
+
'> 2. Continue on the stack-neutral floor anyway — `ui-apply-generic` ' +
|
|
219
|
+
'plus its companions. Output will not be idiomatic for either ' +
|
|
220
|
+
'framework; say so in the result.',
|
|
221
|
+
'> 3. Abort — drop this UI request.',
|
|
222
|
+
);
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Render the "load these skills" line for a dispatch halt.
|
|
228
|
+
*
|
|
229
|
+
* The engine cannot load a skill itself; naming the bundle is what stops the
|
|
230
|
+
* agent from resolving an unresolvable verb by guessing. A lane with no
|
|
231
|
+
* framework-specific executor says that outright rather than presenting the
|
|
232
|
+
* generic pair as if it were stack support.
|
|
233
|
+
*/
|
|
234
|
+
export function bundle_line(
|
|
235
|
+
stack_state: unknown,
|
|
236
|
+
role: 'build' | 'review',
|
|
237
|
+
stack_label?: string,
|
|
238
|
+
): string {
|
|
239
|
+
const bundle = bundle_for(stack_state);
|
|
240
|
+
const stack =
|
|
241
|
+
stack_label ??
|
|
242
|
+
(typeof stack_state === 'string'
|
|
243
|
+
? stack_state
|
|
244
|
+
: String(
|
|
245
|
+
(stack_state as Record<string, unknown> | null)?.['frontend'] ?? 'plain',
|
|
246
|
+
));
|
|
247
|
+
const skills = role === 'review' ? bundle.review : bundle.build;
|
|
248
|
+
const rendered = skills.map((name) => `\`${name}\``).join(' + ');
|
|
249
|
+
if (NATIVELY_GENERIC_LANES.has(stack)) {
|
|
250
|
+
return `> Skills: ${rendered} — stack-neutral, which is what \`${stack}\` means.`;
|
|
251
|
+
}
|
|
252
|
+
if (GENERIC_LANES.has(stack)) {
|
|
253
|
+
return (
|
|
254
|
+
`> Skills: ${rendered} — there is no \`${stack}\`-specific executor, ` +
|
|
255
|
+
'so this is the stack-neutral floor, not idiomatic ' +
|
|
256
|
+
`\`${stack}\`. Say so in the result.`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return `> Skills: ${rendered}.`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Axis-driven composition ─────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Overlay table: one axis value → the skills that add its idiom.
|
|
266
|
+
*
|
|
267
|
+
* This is what replaces lane enumeration. A lane is no longer a hand-written
|
|
268
|
+
* bundle but the base plus whatever the axes turn on, so a stack the package has
|
|
269
|
+
* never named still composes: `reactivity: livewire` pulls Livewire **with or
|
|
270
|
+
* without** Flux, and `component_lib: flux` layers Flux on top. The two
|
|
271
|
+
* full-match lanes fall out as a special case — that identity is the regression
|
|
272
|
+
* witness, asserted in `ui_lane_matrix.test.ts`.
|
|
273
|
+
*
|
|
274
|
+
* Only axis values with a real overlay skill appear. Everything else is served
|
|
275
|
+
* by the base plus its corpus query, which is the point: coverage without a
|
|
276
|
+
* skill per framework.
|
|
277
|
+
*/
|
|
278
|
+
const _AXIS_OVERLAYS: Readonly<Record<string, ReadonlyArray<string>>> = {
|
|
279
|
+
'reactivity:livewire': ['livewire'],
|
|
280
|
+
'reactivity:react': ['react-shadcn-ui'],
|
|
281
|
+
'component_lib:flux': ['flux'],
|
|
282
|
+
'component_lib:shadcn': ['react-shadcn-ui'],
|
|
283
|
+
'component_lib:radix': ['react-shadcn-ui'],
|
|
284
|
+
'view:blade': ['blade-ui'],
|
|
285
|
+
'meta:filament': ['livewire', 'blade-ui'],
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/** Axes consulted for overlays, most-specific first. */
|
|
289
|
+
const _OVERLAY_AXES: ReadonlyArray<string> = [
|
|
290
|
+
'component_lib',
|
|
291
|
+
'meta',
|
|
292
|
+
'reactivity',
|
|
293
|
+
'view',
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
/** Skill → the pack it ships in, for the pack-agnostic determination. */
|
|
297
|
+
const _FRAMEWORK_PACK_SKILLS: ReadonlySet<string> = new Set([
|
|
298
|
+
'livewire',
|
|
299
|
+
'flux',
|
|
300
|
+
'blade-ui',
|
|
301
|
+
'react-shadcn-ui',
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Derive a bundle from detected axes.
|
|
306
|
+
*
|
|
307
|
+
* Order is deliberate and matches the hand-written lanes: overlays first
|
|
308
|
+
* (most-specific axis first), base last. An overlay carries framework idiom the
|
|
309
|
+
* base cannot, so it must be read before the generic contract's fallbacks.
|
|
310
|
+
*
|
|
311
|
+
* `pack_agnostic` is computed, not declared: a composition that pulled in any
|
|
312
|
+
* framework-pack skill is not safe as a fallback, and computing it removes the
|
|
313
|
+
* chance of a hand-written `true` outliving the bundle it described.
|
|
314
|
+
*/
|
|
315
|
+
export function compose_bundle(axes: Readonly<Record<string, string>>): StackBundle {
|
|
316
|
+
const build: string[] = [];
|
|
317
|
+
for (const axis of _OVERLAY_AXES) {
|
|
318
|
+
const value = axes[axis];
|
|
319
|
+
if (value === undefined || value === 'none' || value === 'unknown') continue;
|
|
320
|
+
for (const skill of _AXIS_OVERLAYS[`${axis}:${value}`] ?? []) {
|
|
321
|
+
if (!build.includes(skill)) build.push(skill);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const skill of GENERIC_BUILD) {
|
|
325
|
+
if (!build.includes(skill)) build.push(skill);
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
build,
|
|
329
|
+
review: GENERIC_REVIEW,
|
|
330
|
+
pack_agnostic: !build.some((s) => _FRAMEWORK_PACK_SKILLS.has(s)),
|
|
331
|
+
};
|
|
332
|
+
}
|