@event4u/agent-config 9.11.0 → 9.13.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.
Files changed (183) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +80 -204
  3. package/README.md +8 -1
  4. package/dist/agent-src/commands/fix/ci.md +29 -0
  5. package/dist/agent-src/commands/pr/create.md +60 -0
  6. package/dist/agent-src/contexts/authority/kernel-rule-edits.md +18 -2
  7. package/dist/agent-src/rules/active-remediation.md +0 -2
  8. package/dist/agent-src/rules/architecture.md +0 -1
  9. package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -4
  10. package/dist/agent-src/rules/autonomous-execution.md +1 -2
  11. package/dist/agent-src/rules/broken-access-control.md +0 -2
  12. package/dist/agent-src/rules/code-comment-discipline.md +0 -3
  13. package/dist/agent-src/rules/code-provenance.md +0 -3
  14. package/dist/agent-src/rules/communication-through-line.md +2 -3
  15. package/dist/agent-src/rules/content-quoting-floor.md +0 -2
  16. package/dist/agent-src/rules/context-hygiene.md +1 -3
  17. package/dist/agent-src/rules/decision-revisit-gate.md +0 -2
  18. package/dist/agent-src/rules/design-fidelity.md +30 -1
  19. package/dist/agent-src/rules/domain-adoption-policy.md +0 -3
  20. package/dist/agent-src/rules/downstream-changes.md +10 -1
  21. package/dist/agent-src/rules/external-reference-deep-dive.md +0 -7
  22. package/dist/agent-src/rules/fast-path-marker-visibility.md +0 -1
  23. package/dist/agent-src/rules/git-history-discipline.md +0 -7
  24. package/dist/agent-src/rules/guidelines.md +0 -2
  25. package/dist/agent-src/rules/improve-before-implement.md +4 -2
  26. package/dist/agent-src/rules/invite-challenge.md +0 -4
  27. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -2
  28. package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
  29. package/dist/agent-src/rules/minimal-safe-diff.md +2 -2
  30. package/dist/agent-src/rules/missing-tool-handling.md +0 -1
  31. package/dist/agent-src/rules/no-attribution-footers.md +0 -3
  32. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +8 -5
  33. package/dist/agent-src/rules/no-pr-progress-comments.md +1 -3
  34. package/dist/agent-src/rules/no-roadmap-references.md +0 -2
  35. package/dist/agent-src/rules/output-discipline.md +0 -1
  36. package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -2
  37. package/dist/agent-src/rules/preservation-guard.md +0 -3
  38. package/dist/agent-src/rules/role-mode-adherence.md +0 -1
  39. package/dist/agent-src/rules/secret-vcs-guard.md +0 -2
  40. package/dist/agent-src/rules/senior-engineering-discipline.md +0 -2
  41. package/dist/agent-src/rules/session-canary.md +0 -2
  42. package/dist/agent-src/rules/size-enforcement.md +0 -5
  43. package/dist/agent-src/rules/source-confidentiality.md +0 -3
  44. package/dist/agent-src/rules/source-discovery-gate.md +0 -2
  45. package/dist/agent-src/rules/think-before-action.md +3 -3
  46. package/dist/agent-src/rules/token-budget-discipline.md +0 -1
  47. package/dist/agent-src/rules/token-efficiency.md +1 -2
  48. package/dist/agent-src/rules/tool-safety.md +0 -1
  49. package/dist/agent-src/rules/user-interaction.md +0 -3
  50. package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
  51. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +8 -0
  52. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +1 -0
  53. package/dist/agent-src/skills/analysis-skill-router/SKILL.md +55 -86
  54. package/dist/agent-src/skills/character-consistency/SKILL.md +7 -4
  55. package/dist/agent-src/skills/decision-record/SKILL.md +8 -0
  56. package/dist/agent-src/skills/design-intelligence/SKILL.md +27 -1
  57. package/dist/agent-src/skills/design-intelligence/data/font-pairings-reference.csv +74 -74
  58. package/dist/agent-src/skills/design-intelligence/data/manifest.json +2 -0
  59. package/dist/agent-src/skills/design-review/SKILL.md +11 -0
  60. package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +8 -0
  61. package/dist/agent-src/skills/design-tokens/scripts/tokens.ts +0 -7
  62. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +34 -3
  63. package/dist/agent-src/skills/fe-design/SKILL.md +10 -0
  64. package/dist/agent-src/skills/motion-choreographer/SKILL.md +7 -4
  65. package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +201 -0
  66. package/dist/agent-src/skills/performance/SKILL.md +14 -0
  67. package/dist/agent-src/skills/pixar-storyteller/SKILL.md +8 -5
  68. package/dist/agent-src/skills/scene-expander/SKILL.md +9 -6
  69. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +10 -0
  70. package/dist/agent-src/skills/typography-system/SKILL.md +55 -11
  71. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +180 -0
  72. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +14 -37
  73. package/dist/agent-src/skills/video-director/SKILL.md +9 -6
  74. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  75. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
  76. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
  77. package/dist/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
  78. package/dist/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
  79. package/dist/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
  80. package/dist/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
  81. package/dist/agent-src/templates/scripts/work_engine/state.ts +55 -0
  82. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  83. package/dist/cli-delegate/{chunk-IR7P7JWA.js → chunk-A3HJ6NTI.js} +271 -59
  84. package/dist/cli-delegate/chunk-EJYIGZ3T.js +125 -0
  85. package/dist/cli-delegate/{chunk-RDGRHAAB.js → chunk-HYWSRNUX.js} +6 -86
  86. package/dist/cli-delegate/{chunk-YMFO6FDN.js → chunk-QFKQLQ7X.js} +313 -81
  87. package/dist/cli-delegate/{chunk-4SV3XGFA.js → chunk-TEGJU2JX.js} +7 -9
  88. package/dist/cli-delegate/chunk-TJIAM5FW.js +133 -0
  89. package/dist/cli-delegate/cmd_conformance.js +4 -7
  90. package/dist/cli-delegate/cmd_doctor.js +3 -5
  91. package/dist/cli-delegate/cmd_explain.js +153 -85
  92. package/dist/cli-delegate/cmd_export.js +5 -3
  93. package/dist/cli-delegate/cmd_fleet.js +2 -1
  94. package/dist/cli-delegate/cmd_preflight.js +6 -123
  95. package/dist/cli-delegate/cmd_prune.js +5 -3
  96. package/dist/cli-delegate/cmd_settings_migrate.js +5 -3
  97. package/dist/cli-delegate/cmd_sync.js +5 -3
  98. package/dist/cli-delegate/cmd_uninstall.js +6 -5
  99. package/dist/cli-delegate/cmd_validate.js +5 -3
  100. package/dist/discovery/deprecation-report.md +1 -1
  101. package/dist/discovery/discovery-manifest.json +116 -72
  102. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  103. package/dist/discovery/discovery-manifest.summary.md +3 -3
  104. package/dist/discovery/orphan-report.md +1 -1
  105. package/dist/discovery/packs.json +7 -5
  106. package/dist/discovery/trust-report.md +2 -2
  107. package/dist/discovery/workspaces.json +6 -4
  108. package/dist/hooks/dispatch.js +174 -13
  109. package/dist/install/install.mjs +605 -412
  110. package/dist/install/ruleInScope.js +123 -0
  111. package/dist/install/ruleInScope.js.map +1 -0
  112. package/dist/install/rule_scope.js +2 -2
  113. package/dist/install/rule_scope.js.map +1 -1
  114. package/dist/install/wizard-plan.js +10 -1
  115. package/dist/install/wizard-plan.js.map +1 -1
  116. package/dist/mcp/registry-manifest.json +3 -3
  117. package/dist/mcp/server.json +2 -2
  118. package/dist/mcp/server.mjs +710 -2491
  119. package/dist/router.json +1 -1
  120. package/dist/scripts/condense.js +8 -24
  121. package/dist/scripts/condense.js.map +1 -1
  122. package/docs/MIGRATION.md +1 -1
  123. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +71 -0
  124. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +24 -1
  125. package/docs/guidelines/code-clarity.md +9 -2
  126. package/docs/guidelines/design-fidelity-mechanics.md +58 -0
  127. package/llms.txt +63 -27
  128. package/package.json +4 -4
  129. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +162 -17
  130. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +118 -0
  131. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +49 -6
  132. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +9 -5
  133. package/src/agent-src/templates/scripts/work_engine/directives/ui/stack_bundles.ts +182 -29
  134. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +311 -24
  135. package/src/agent-src/templates/scripts/work_engine/state.ts +55 -0
  136. package/src/config/agent-settings.template.yml +8 -0
  137. package/src/install/ruleInScope.ts +133 -0
  138. package/src/install/rule_scope.ts +2 -2
  139. package/src/install/wizard-plan.ts +11 -1
  140. package/src/scripts/_lib/counted_probe.ts +142 -0
  141. package/src/scripts/_lib/global_deploy_inventory.ts +38 -2
  142. package/src/scripts/_lib/overbuild_lens_contract.ts +196 -0
  143. package/src/scripts/_lib/release_scope.ts +207 -0
  144. package/src/scripts/ai_council/stance_tally.ts +37 -2
  145. package/src/scripts/audit_auto_rules.ts +9 -9
  146. package/src/scripts/audit_likelihood.ts +3 -3
  147. package/src/scripts/audit_overlap.ts +2 -2
  148. package/src/scripts/audit_skill_overlap.ts +233 -95
  149. package/src/scripts/audit_skill_overlap_allowlist.json +4 -0
  150. package/src/scripts/build_proof.ts +15 -1
  151. package/src/scripts/check_dependency_floors.ts +16 -6
  152. package/src/scripts/check_gate_coverage.ts +6 -6
  153. package/src/scripts/check_installer_import_purity.ts +295 -0
  154. package/src/scripts/check_kernel_rule_bundle.ts +5 -4
  155. package/src/scripts/check_token_quality_golden.ts +0 -18
  156. package/src/scripts/code_graph/loader.ts +35 -6
  157. package/src/scripts/compile_router.ts +3 -8
  158. package/src/scripts/condense.ts +9 -30
  159. package/src/scripts/explain_run.ts +0 -1
  160. package/src/scripts/generate_pack_manifests.ts +3 -20
  161. package/src/scripts/hooks/block_kernel_rule_writes.ts +145 -10
  162. package/src/scripts/hooks/block_no_verify.ts +101 -0
  163. package/src/scripts/install-hooks.sh +36 -4
  164. package/src/scripts/install.ts +210 -5
  165. package/src/scripts/lint_agent_security.ts +5 -4
  166. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  167. package/src/scripts/lint_regression.ts +92 -1
  168. package/src/scripts/lint_skill_descriptions.ts +93 -5
  169. package/src/scripts/lint_ui_stack_bundles.ts +31 -0
  170. package/src/scripts/project_thin_rules.ts +2 -2
  171. package/src/scripts/release.ts +107 -3
  172. package/src/scripts/resolve_lint_scope.ts +53 -0
  173. package/src/scripts/router_telemetry.ts +0 -5
  174. package/src/scripts/runtime_dispatcher.ts +40 -0
  175. package/src/scripts/schemas/pack.schema.json +1 -5
  176. package/src/scripts/schemas/rule.schema.json +1 -2
  177. package/src/scripts/self_review_gate.ts +15 -73
  178. package/src/scripts/skill_linter.ts +93 -11
  179. package/src/scripts/sweep_dead_scan_roots.ts +550 -0
  180. package/src/scripts/trigger_coverage.ts +11 -37
  181. package/dist/cli-delegate/chunk-7DSAMZFU.js +0 -356
  182. package/dist/cli-delegate/chunk-BJGXZNPM.js +0 -266
  183. package/dist/cli-delegate/chunk-TFG57JJM.js +0 -1969
@@ -44,6 +44,33 @@ export const PLACEHOLDER_PATTERNS: ReadonlyArray<string> = [
44
44
  'xxx',
45
45
  ];
46
46
 
47
+ /**
48
+ * Optional brief key carrying a user-supplied design artifact.
49
+ *
50
+ * The brief's five required keys are a lossy projection of a finished
51
+ * artifact: `layout` is prose, not a spacing scale, and there is no slot at
52
+ * all for easing, hover behaviour, handlers, or assets. Before this key the
53
+ * pipeline had no channel for a handed-over design, so it rebuilt one from
54
+ * the five-key abstraction and said nothing about the difference. This slot
55
+ * is that channel — one more input to the same step, never a parallel track.
56
+ */
57
+ export const PROVIDED_ARTIFACT_KEY = 'provided_artifact';
58
+
59
+ /**
60
+ * What the five-key brief cannot carry from a provided artifact.
61
+ *
62
+ * Stated to the user **before** any regeneration, not discovered afterwards.
63
+ * A supplied `design-system.json` closes the first two; the rest are closed
64
+ * only by the coverage report `apply` demands.
65
+ */
66
+ export const UNCARRIED_BY_THE_BRIEF: ReadonlyArray<string> = [
67
+ 'exact spacing values — `layout` is prose, not a scale',
68
+ 'easing curves and animation timing',
69
+ 'hover / focus / active behaviour',
70
+ 'event handlers and the interactions they drive',
71
+ 'the asset manifest (images, icons, fonts the artifact references)',
72
+ ];
73
+
47
74
  export const AMBIGUITIES: ReadonlyArray<Record<string, string>> = [
48
75
  {
49
76
  code: 'design_missing',
@@ -61,6 +88,17 @@ export const AMBIGUITIES: ReadonlyArray<Record<string, string>> = [
61
88
  'agent re-runs the brief with final strings; ' +
62
89
  'halt lists the offending microcopy keys',
63
90
  },
91
+ {
92
+ code: 'design_provided_without_contract',
93
+ trigger:
94
+ 'state.ui_design.provided_artifact is set but carries no ' +
95
+ 'design_system, and the loss has not been acknowledged — the ' +
96
+ 'brief would silently regenerate what it cannot carry',
97
+ resolution:
98
+ 'user supplies a design-system.json (honoured verbatim), or ' +
99
+ 'acknowledges the named losses so the port proceeds with them ' +
100
+ 'stated up front',
101
+ },
64
102
  {
65
103
  code: 'design_unconfirmed',
66
104
  trigger: 'brief is well-formed but design_confirmed is unset/False',
@@ -123,6 +161,11 @@ export function run(state: DeliveryState): StepResult {
123
161
  return _halt_placeholders(state, placeholders);
124
162
  }
125
163
 
164
+ const provided = provided_artifact(designDict);
165
+ if (provided !== null && !_contract_or_loss_acknowledged(provided)) {
166
+ return _halt_provided_without_contract(state, provided);
167
+ }
168
+
126
169
  if (designDict['design_confirmed'] === true) {
127
170
  return new StepResult({ outcome: Outcome.SUCCESS });
128
171
  }
@@ -238,6 +281,81 @@ function _walk_placeholders(node: Any, prefix: string, violations: string[]): vo
238
281
  }
239
282
  }
240
283
 
284
+ /**
285
+ * Return the supplied-artifact envelope, or `null` when none was handed over.
286
+ *
287
+ * Exported because `apply` needs the same answer — the coverage report it
288
+ * demands exists only for a port, and a second copy of "is this a port?"
289
+ * would be the drift this package has paid for before (see the shared
290
+ * `placeholder_paths` walker above).
291
+ */
292
+ export function provided_artifact(
293
+ design: Record<string, Any> | null | undefined,
294
+ ): Record<string, Any> | null {
295
+ if (!_isDict(design)) return null;
296
+ const provided = design[PROVIDED_ARTIFACT_KEY];
297
+ if (!_isDict(provided) || Object.keys(provided).length === 0) return null;
298
+ return provided;
299
+ }
300
+
301
+ /** True when a machine-readable contract came with the artifact. */
302
+ export function has_design_system(provided: Record<string, Any>): boolean {
303
+ return _isDict(provided['design_system']) && _pyTruthy(provided['design_system']);
304
+ }
305
+
306
+ /** The port may proceed: either a contract exists, or the losses were stated and accepted. */
307
+ function _contract_or_loss_acknowledged(provided: Record<string, Any>): boolean {
308
+ return has_design_system(provided) || provided['loss_acknowledged'] === true;
309
+ }
310
+
311
+ /**
312
+ * BLOCKED halt — an artifact was handed over with no contract behind it.
313
+ *
314
+ * This is the honest-refusal branch. The pipeline *can* rebuild the artifact
315
+ * from the five-key brief; what it must not do is rebuild it silently. The
316
+ * halt names every value the brief cannot carry, then asks. Answering
317
+ * option 1 sets `loss_acknowledged` and the same losses are on the record.
318
+ */
319
+ function _halt_provided_without_contract(
320
+ state: DeliveryState,
321
+ provided: Record<string, Any>,
322
+ ): StepResult {
323
+ const preview = _preview_input(state);
324
+ const ref = typeof provided['path'] === 'string' && provided['path'] !== ''
325
+ ? String(provided['path'])
326
+ : 'the provided artifact';
327
+ const lines: string[] = [
328
+ `> Input: ${preview}`,
329
+ `> \`${ref}\` is a finished design handed over as the spec, and no ` +
330
+ '`design-system.json` came with it. The design brief carries five ' +
331
+ 'keys (layout, components, states, microcopy, a11y) — everything ' +
332
+ 'below is outside them and would be rebuilt from taste rather than ' +
333
+ 'read from your artifact:',
334
+ ];
335
+ for (const loss of UNCARRIED_BY_THE_BRIEF) {
336
+ lines.push(`> - ${loss}`);
337
+ }
338
+ lines.push(
339
+ '> 1. Proceed — port it with those five losses on the record; `apply` ' +
340
+ 'still has to report what it honoured, translated, and flagged',
341
+ '> 2. Supply a `design-system.json` — its token values are then ' +
342
+ 'honoured verbatim instead of re-derived',
343
+ '> 3. Abort — drop this UI request',
344
+ '',
345
+ '**Recommendation: 2 — Supply the contract** — it is the only option ' +
346
+ 'that closes the spacing and easing losses at the source. Caveat: ' +
347
+ 'pick 1 when no extractor is available; the port then proceeds with ' +
348
+ 'the gap stated rather than hidden.',
349
+ );
350
+ return new StepResult({
351
+ outcome: Outcome.BLOCKED,
352
+ questions: lines,
353
+ message:
354
+ 'UI design halted: a provided artifact carries no design-system.json; ' +
355
+ `${UNCARRIED_BY_THE_BRIEF.length} value class(es) cannot be carried by the brief.`,
356
+ });
357
+ }
358
+
241
359
  /** Render a one-line preview of the input being designed. */
242
360
  function _preview_input(state: DeliveryState): string {
243
361
  const data = (_isDict(state.ticket) ? state.ticket : {}) as Record<string, Any>;
@@ -16,7 +16,7 @@ import {
16
16
  agent_directive,
17
17
  } from '../../delivery_state.js';
18
18
  import {
19
- UNSUPPORTED_LANE,
19
+ is_ambiguous_stack,
20
20
  bundle_line,
21
21
  unsupported_stack_questions,
22
22
  } from './stack_bundles.js';
@@ -116,6 +116,41 @@ function _pyTruthy(value: Any): boolean {
116
116
  return true;
117
117
  }
118
118
 
119
+ /**
120
+ * Findings the provided artifact already answers — informational, never
121
+ * fix-worthy.
122
+ *
123
+ * A port carries its source's aesthetic on purpose. Left unfiltered, a
124
+ * finding like "cream palette reads as scaffold-default" is indistinguishable
125
+ * from a real defect, so the polish loop spends a round editing the port away
126
+ * from the spec the user approved — the exact regression `daf-slop-vs-provided`
127
+ * witnesses. Marking the finding rather than deleting it keeps it visible in
128
+ * the review output; what changes is that it no longer drives a round.
129
+ *
130
+ * Scope is strict, and the burden sits with the marker: only a decision the
131
+ * artifact **actually covers** may carry the flag. Generative work in the same
132
+ * run is not artifact-covered and stays fully subject to the anti-slop scan.
133
+ */
134
+ export const ARTIFACT_COVERED_KEY = 'artifact_covered';
135
+
136
+ /** True when this finding is answered by the provided artifact. */
137
+ function _is_artifact_covered(finding: Any): boolean {
138
+ return _isDict(finding) && finding[ARTIFACT_COVERED_KEY] === true;
139
+ }
140
+
141
+ /** Split findings into the ones that drive rounds and the informational rest. */
142
+ export function partition_artifact_covered(findings: ReadonlyArray<Any>): {
143
+ actionable: Any[];
144
+ informational: Any[];
145
+ } {
146
+ const actionable: Any[] = [];
147
+ const informational: Any[] = [];
148
+ for (const finding of findings) {
149
+ (_is_artifact_covered(finding) ? informational : actionable).push(finding);
150
+ }
151
+ return { actionable, informational };
152
+ }
153
+
119
154
  /** Apply the polish-loop gate. */
120
155
  export function run(state: DeliveryState): StepResult {
121
156
  const review = _pyTruthy(state.ui_review) ? (state.ui_review as Record<string, Any>) : {};
@@ -125,6 +160,11 @@ export function run(state: DeliveryState): StepResult {
125
160
  }
126
161
  const review_clean = _pyTruthy(review['review_clean']);
127
162
 
163
+ // Artifact-covered findings are dropped from the round-driving set before
164
+ // anything else reads it — including the ceiling check, so a port cannot
165
+ // burn its two rounds on findings it was never allowed to act on.
166
+ findings = partition_artifact_covered(findings as Any[]).actionable;
167
+
128
168
  if (review_clean || (findings as Any[]).length === 0) {
129
169
  return new StepResult({ outcome: Outcome.SUCCESS });
130
170
  }
@@ -195,12 +235,15 @@ function _delegate_to_polish_skill(
195
235
  ceiling: number,
196
236
  ): StepResult {
197
237
  const directive = _resolve_directive(state);
198
- const unsupported_label = _stack_label(state);
199
- if (unsupported_label === UNSUPPORTED_LANE) {
238
+ if (is_ambiguous_stack(state.stack)) {
239
+ const conflicts = ((state.stack as Record<string, Any>)['ambiguity'] ??
240
+ []) as ReadonlyArray<string>;
200
241
  return new StepResult({
201
242
  outcome: Outcome.BLOCKED,
202
- questions: unsupported_stack_questions('polish'),
203
- message: 'UI polish refused: frontend framework not modelled by any lane.',
243
+ questions: unsupported_stack_questions('polish', conflicts),
244
+ message:
245
+ `UI polish halted: detection is ambiguous (${conflicts.join('; ')}) — ` +
246
+ 'the open question is which project to build for.',
204
247
  });
205
248
  }
206
249
  const stack_label = _stack_label(state);
@@ -222,7 +265,7 @@ function _delegate_to_polish_skill(
222
265
  questions: [
223
266
  agent_directive(directive),
224
267
  `> Stack: \`${stack_label}\`. Polish round ${next_round} of ${ceiling}.`,
225
- bundle_line(stack_label, 'build'),
268
+ bundle_line(state.stack, 'build', stack_label),
226
269
  findings_line,
227
270
  '> Fix chart-type / contrast findings against the adopted ' +
228
271
  "corpus rows (design-intelligence § 'Grounding the " +
@@ -17,7 +17,7 @@ import {
17
17
  agent_directive,
18
18
  } from '../../delivery_state.js';
19
19
  import {
20
- UNSUPPORTED_LANE,
20
+ is_ambiguous_stack,
21
21
  bundle_line,
22
22
  unsupported_stack_questions,
23
23
  } from './stack_bundles.js';
@@ -192,11 +192,15 @@ function _stack_label(state: DeliveryState): string {
192
192
  function _delegate_to_review_skill(state: DeliveryState): StepResult {
193
193
  const directive = _resolve_directive(state);
194
194
  const stack_label = _stack_label(state);
195
- if (stack_label === UNSUPPORTED_LANE) {
195
+ if (is_ambiguous_stack(state.stack)) {
196
+ const conflicts = ((state.stack as Record<string, Any>)['ambiguity'] ??
197
+ []) as ReadonlyArray<string>;
196
198
  return new StepResult({
197
199
  outcome: Outcome.BLOCKED,
198
- questions: unsupported_stack_questions('review'),
199
- message: 'UI review refused: frontend framework not modelled by any lane.',
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.',
200
204
  });
201
205
  }
202
206
  return new StepResult({
@@ -205,7 +209,7 @@ function _delegate_to_review_skill(state: DeliveryState): StepResult {
205
209
  agent_directive(directive),
206
210
  `> Stack: \`${stack_label}\`. Reviewing rendered components ` +
207
211
  'against the locked design brief.',
208
- bundle_line(stack_label, 'review'),
212
+ bundle_line(state.stack, 'review', stack_label),
209
213
  '> The review pass compares `state.ticket.ui_apply.rendered` ' +
210
214
  'against `state.ui_design` (microcopy, states, a11y, layout) ' +
211
215
  'and produces a structured `findings` list.',
@@ -39,14 +39,24 @@ export interface StackBundle {
39
39
  }
40
40
 
41
41
  /**
42
- * The pack-agnostic pair. Both live in `engineering-base`, so they are present
43
- * in every install; both self-describe as stack-neutral
44
- * (`ui-component-architect`: "the same lens applies to Blade, Livewire, React,
45
- * or Vue trees"; `tailwind-engineer`: "the **how** for any Tailwind-stack
46
- * screen"). This is the honest floor for a lane with no framework-specific
47
- * executor — not a claim that the lane is fully served.
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").
48
54
  */
49
- const GENERIC_BUILD: ReadonlyArray<string> = ['ui-component-architect', 'tailwind-engineer'];
55
+ const GENERIC_BUILD: ReadonlyArray<string> = [
56
+ 'ui-apply-generic',
57
+ 'ui-component-architect',
58
+ 'tailwind-engineer',
59
+ ];
50
60
 
51
61
  /** `design-review` is stack-neutral and ships in `engineering-base`. */
52
62
  const GENERIC_REVIEW: ReadonlyArray<string> = ['design-review'];
@@ -57,6 +67,16 @@ const GENERIC_REVIEW: ReadonlyArray<string> = ['design-review'];
57
67
  * contract) and consume this map only to name the skills in the halt body.
58
68
  */
59
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
+ },
60
80
  'blade-livewire-flux': {
61
81
  build: ['flux', 'livewire', 'blade-ui'],
62
82
  review: GENERIC_REVIEW,
@@ -113,7 +133,7 @@ export const STACK_BUNDLES: Readonly<Record<string, StackBundle>> = {
113
133
  * definite idiom and the package has no executor for it, so the result must not
114
134
  * read as Vue support.
115
135
  */
116
- export const GENERIC_LANES: ReadonlySet<string> = new Set(['vue', 'plain']);
136
+ export const GENERIC_LANES: ReadonlySet<string> = new Set(['vue', 'plain', 'unknown']);
117
137
 
118
138
  /** Lanes where the stack-neutral pair is the correct answer, not a shortfall. */
119
139
  const NATIVELY_GENERIC_LANES: ReadonlySet<string> = new Set(['plain']);
@@ -126,31 +146,81 @@ const NATIVELY_GENERIC_LANES: ReadonlySet<string> = new Set(['plain']);
126
146
  */
127
147
  export const UNSUPPORTED_LANE = 'unknown';
128
148
 
129
- /** Return the bundle for `stack`, or the `plain` bundle when unmapped. */
130
- export function bundle_for(stack: string): StackBundle {
131
- return STACK_BUNDLES[stack] ?? (STACK_BUNDLES['plain'] as StackBundle);
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;
132
194
  }
133
195
 
134
196
  /**
135
- * Questions for the refusal halt on an unmodelled framework.
197
+ * Questions for the refusal halt when detection cannot say what the project is.
136
198
  *
137
- * Not a directive — there is nothing honest to dispatch to. The agent names
138
- * the actual markers from the manifest (it has filesystem access; the engine
139
- * deliberately does not carry them on `StackResult`), and the user chooses
140
- * between the stack-neutral floor and stopping.
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.
141
202
  */
142
- export function unsupported_stack_questions(step: string): string[] {
143
- return [
144
- '> Detection found a frontend framework this package does not model, ' +
145
- `so there is no \`${step}\` lane for it.`,
146
- '> Name the markers you see in `composer.json` / `package.json` so the ' +
147
- 'choice below is made on the real stack, not on a guess.',
148
- '> 1. Continue on the stack-neutral floor — `ui-component-architect` + ' +
149
- '`tailwind-engineer`. Output will not be idiomatic for this ' +
150
- 'framework; say so in the result.',
151
- '> 2. Abort — drop this UI request and add a lane first ' +
152
- '(`docs/contracts/ui-stack-extension.md`).',
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.',
153
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;
154
224
  }
155
225
 
156
226
  /**
@@ -161,8 +231,19 @@ export function unsupported_stack_questions(step: string): string[] {
161
231
  * framework-specific executor says that outright rather than presenting the
162
232
  * generic pair as if it were stack support.
163
233
  */
164
- export function bundle_line(stack: string, role: 'build' | 'review'): string {
165
- const bundle = bundle_for(stack);
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
+ ));
166
247
  const skills = role === 'review' ? bundle.review : bundle.build;
167
248
  const rendered = skills.map((name) => `\`${name}\``).join(' + ');
168
249
  if (NATIVELY_GENERIC_LANES.has(stack)) {
@@ -177,3 +258,75 @@ export function bundle_line(stack: string, role: 'build' | 'review'): string {
177
258
  }
178
259
  return `> Skills: ${rendered}.`;
179
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
+ }