@event4u/agent-config 8.4.0 → 8.4.1

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 (126) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  4. package/dist/discovery/deprecation-report.md +1 -1
  5. package/dist/discovery/discovery-manifest.json +1 -1
  6. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  7. package/dist/discovery/discovery-manifest.summary.md +1 -1
  8. package/dist/discovery/orphan-report.md +1 -1
  9. package/dist/discovery/packs.json +1 -1
  10. package/dist/discovery/trust-report.md +1 -1
  11. package/dist/discovery/workspaces.json +1 -1
  12. package/dist/mcp/registry-manifest.json +1 -1
  13. package/package.json +4 -1
  14. package/src/agent-src/README.md +64 -0
  15. package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
  16. package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
  17. package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
  18. package/src/agent-src/templates/AGENTS.md +34 -0
  19. package/src/agent-src/templates/scripts/README.md +195 -0
  20. package/src/agent-src/templates/scripts/check_memory.ts +662 -0
  21. package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
  22. package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
  23. package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
  24. package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
  25. package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
  26. package/src/agent-src/templates/scripts/memory_report.ts +868 -0
  27. package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
  28. package/src/agent-src/templates/scripts/memory_status.ts +215 -0
  29. package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
  30. package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
  31. package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
  32. package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
  33. package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
  34. package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
  35. package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
  36. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
  37. package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
  38. package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
  39. package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
  40. package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
  41. package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
  42. package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
  43. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
  44. package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
  45. package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
  46. package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
  47. package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
  48. package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
  49. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
  50. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
  51. package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
  52. package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
  53. package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
  54. package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
  55. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
  56. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
  57. package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
  58. package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
  59. package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
  60. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
  61. package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
  62. package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
  63. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
  64. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
  65. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
  66. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
  67. package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
  68. package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
  73. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
  74. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
  75. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
  76. package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
  77. package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
  78. package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
  79. package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
  80. package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
  81. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
  82. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
  83. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
  84. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
  85. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
  86. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
  87. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
  88. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
  89. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
  90. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
  91. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
  92. package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
  93. package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
  94. package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
  95. package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
  96. package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
  97. package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
  98. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
  99. package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
  100. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
  101. package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
  102. package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
  103. package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
  104. package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
  105. package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
  106. package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
  107. package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
  108. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
  109. package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
  110. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
  111. package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
  112. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
  113. package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
  114. package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
  115. package/src/install/atomic.ts +111 -0
  116. package/src/install/conflict.ts +226 -0
  117. package/src/install/detect.ts +280 -0
  118. package/src/install/paths.ts +90 -0
  119. package/src/install/plan.ts +205 -0
  120. package/src/install/preflight.ts +210 -0
  121. package/src/install/selectedTools.ts +52 -0
  122. package/src/install/toolDetection.ts +120 -0
  123. package/src/install/txlog.ts +161 -0
  124. package/src/install/types.ts +175 -0
  125. package/src/install/wizard-plan.ts +216 -0
  126. package/src/scripts/prepack-check.mjs +108 -5
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Heuristic intent classifier — see `work_engine.intent` for context.
3
+ *
4
+ * TypeScript twin of `work_engine/intent/classify.py` (ADR-200 py2ts —
5
+ * work_engine foundation). Public API names stay snake_case to mirror the
6
+ * Python module 1:1 (per ADR-200 — Python style is part of the contract).
7
+ *
8
+ * The classifier walks a small priority ladder against the lower-cased
9
+ * prompt + optional ticket title. First match wins; `backend-coding` is
10
+ * the fall-through default so every prompt always lands on a known label.
11
+ *
12
+ * Priority order (deliberately fixed):
13
+ *
14
+ * 1. **Trivial-UI** — UI signal AND a trivial-edit verb pattern (`change
15
+ * color`, `make … red`, `rename label`, `fix copy`) AND no
16
+ * structural verb (`add`, `build`, `create`, `introduce`).
17
+ * 2. **Mixed** — UI signal AND a backend signal (`endpoint`, `API`,
18
+ * `migration`, `schema`, `query`, `job`, `queue`).
19
+ * 3. **UI-Improve** — UI signal AND an improve/redesign/refactor verb,
20
+ * OR explicit "existing" surface markers.
21
+ * 4. **UI-Build** — UI signal AND a build/create/add verb, OR new-screen
22
+ * markers (`new page`, `new screen`, `new component`).
23
+ * 5. **Mixed (full-stack feature)** — a build verb AND a full-stack noun
24
+ * (`feature`, `app`, `module`, `system`, …), with no isolated
25
+ * UI or backend signal. "Build me a feature" / "build the billing
26
+ * module" want the contract-lock → UI → stitch → verify mixed flow,
27
+ * not the backend default (greenfield-scaffold Phase 4).
28
+ * 6. **Backend-Coding** — default.
29
+ *
30
+ * The label is the dispatcher's *only* input for routing. Confidence
31
+ * band, `ui_intent` flag from the scorer, and AC reconstruction stay
32
+ * the resolution surface — the classifier does not look at them.
33
+ */
34
+ import type { WorkState } from '../state.js';
35
+
36
+ export const INTENT_UI_BUILD = 'ui-build';
37
+ export const INTENT_UI_IMPROVE = 'ui-improve';
38
+ export const INTENT_UI_TRIVIAL = 'ui-trivial';
39
+ export const INTENT_MIXED = 'mixed';
40
+ export const INTENT_BACKEND = 'backend-coding';
41
+
42
+ /**
43
+ * All labels the classifier can return.
44
+ *
45
+ * Locked here so the dispatcher's mapping table and the test suite share
46
+ * one source of truth.
47
+ */
48
+ export const KNOWN_INTENTS: ReadonlySet<string> = new Set([
49
+ INTENT_UI_BUILD,
50
+ INTENT_UI_IMPROVE,
51
+ INTENT_UI_TRIVIAL,
52
+ INTENT_MIXED,
53
+ INTENT_BACKEND,
54
+ ]);
55
+
56
+ /**
57
+ * Strong UI nouns — exclusive UI meaning.
58
+ *
59
+ * Deliberately omits `table`, `list`, `input`, and `field`:
60
+ * `table`/`list` collide with database tables and Python/PHP lists;
61
+ * `input`/`field` collide with function inputs, command inputs, and
62
+ * JSON/DB fields. Genuine UI prompts that mean form inputs always come
63
+ * with a strong-UI noun nearby (`form`, `page`, `component`).
64
+ */
65
+ const _UI_NOUNS: ReadonlySet<string> = new Set([
66
+ 'ui', 'screen', 'page', 'view', 'form', 'modal', 'dialog',
67
+ 'button', 'card', 'tile',
68
+ 'header', 'footer', 'nav', 'navigation', 'sidebar', 'menu',
69
+ 'dropdown', 'tab', 'panel', 'layout', 'component', 'icon',
70
+ 'tooltip', 'toast', 'banner', 'badge', 'avatar', 'label',
71
+ 'checkbox', 'radio', 'toggle', 'switch', 'stepper', 'wizard',
72
+ ]);
73
+
74
+ const _UI_STYLE: ReadonlySet<string> = new Set([
75
+ 'color', 'colour', 'css', 'tailwind', 'padding', 'margin',
76
+ 'spacing', 'font', 'typography', 'responsive', 'mobile',
77
+ 'dark mode', 'light mode', 'theme', 'shadow', 'border',
78
+ 'rounded', 'radius',
79
+ ]);
80
+
81
+ const _BACKEND_SIGNALS: ReadonlySet<string> = new Set([
82
+ 'endpoint', 'api', 'route', 'controller', 'service',
83
+ 'migration', 'schema', 'table', 'column', 'index', 'query',
84
+ 'queue', 'job', 'worker', 'webhook', 'policy', 'gate',
85
+ 'command', 'cron', 'broadcast', 'event', 'listener',
86
+ ]);
87
+
88
+ const _TRIVIAL_VERBS: ReadonlySet<string> = new Set([
89
+ 'rename', 'relabel', 'tweak', 'adjust', 'swap', 'change',
90
+ ]);
91
+
92
+ const _IMPROVE_VERBS: ReadonlySet<string> = new Set([
93
+ 'improve', 'polish', 'redesign', 'rework', 'refine',
94
+ 'refactor', 'tighten', 'clean', 'fix', 'update', 'tune',
95
+ ]);
96
+
97
+ const _BUILD_VERBS: ReadonlySet<string> = new Set([
98
+ 'add', 'build', 'create', 'introduce', 'implement', 'ship',
99
+ 'draft', 'scaffold', 'wire',
100
+ ]);
101
+
102
+ const _FULLSTACK_PHRASE: RegExp = /\bfull[ -]?stack\b/;
103
+
104
+ // A full-stack deliverable must be the *object* of a build verb — not
105
+ // merely co-occur in the text. "Build me a feature" / "scaffold the
106
+ // billing module" route to mixed; "returns the integer **product**"
107
+ // (a math result, not a software product) does not, because no build
108
+ // verb governs it. The noun must follow a build verb within a short
109
+ // article/adjective window. Deliberately omits `product` (math
110
+ // polysemy), `page` / `screen` / `component` (pure-UI — the
111
+ // UI-Build rung owns them), and `endpoint` / `api` (pure-backend).
112
+ const _FULLSTACK_BUILD: RegExp =
113
+ /\b(?:build|create|scaffold|implement|ship|draft|wire|introduce|add)\b(?:\s+(?:me|a|an|the|our|my|new|complete|full|entire|whole|single))*(?:\s+[\w-]+){0,2}\s+\b(?:feature|app|application|module|system|mvp|prototype|saas|platform|crud|workflow)\b/;
114
+
115
+ const _NEW_SURFACE: RegExp =
116
+ /\b(new|fresh|blank)\s+(page|screen|view|component|form|modal|tile|dashboard)\b/;
117
+
118
+ const _EXISTING_SURFACE: RegExp =
119
+ /\b(existing|current|the)\s+(page|screen|view|component|form|modal)\b/;
120
+
121
+ const _TRIVIAL_PATTERN: RegExp =
122
+ /\b(make|change|update|set|swap)\b[^.]{0,40}\b(red|blue|green|yellow|black|white|primary|secondary|color|colour|copy|text|label|wording|class|prop)\b/;
123
+
124
+ /**
125
+ * Return one of {@link KNOWN_INTENTS} for the supplied text.
126
+ *
127
+ * @param raw The user prompt or ticket body. Whitespace is normalised
128
+ * internally; `""` and `null` resolve to `backend-coding`.
129
+ * @param title Optional ticket title. Concatenated with `raw` before
130
+ * scanning so single-line ticket headlines (`"Add CSV export"`) produce
131
+ * the same label whether they arrive in the body or the title slot.
132
+ */
133
+ export function classify_intent(
134
+ raw: string,
135
+ opts: { title?: string | null } = {},
136
+ ): string {
137
+ const title = opts.title ?? null;
138
+ // Python: " ".join(filter(None, (title, raw))).strip().lower()
139
+ // filter(None, ...) drops falsy entries (None and "").
140
+ const parts = [title, raw].filter((p): p is string => Boolean(p));
141
+ const text = pyLower(pyStrip(parts.join(' ')));
142
+ if (!text) {
143
+ return INTENT_BACKEND;
144
+ }
145
+
146
+ const has_ui = _has_ui_signal(text);
147
+ const has_backend = _has_backend_signal(text);
148
+
149
+ if (has_ui && _is_trivial(text)) {
150
+ return INTENT_UI_TRIVIAL;
151
+ }
152
+ if (has_ui && has_backend) {
153
+ return INTENT_MIXED;
154
+ }
155
+ if (has_ui && _is_improve(text)) {
156
+ return INTENT_UI_IMPROVE;
157
+ }
158
+ if (has_ui && _is_build(text)) {
159
+ return INTENT_UI_BUILD;
160
+ }
161
+ if (has_ui) {
162
+ // UI signal but no clear verb — default to ui-improve so the
163
+ // full audit gate engages. ui-build would skip the existing-
164
+ // surface check, which is the wrong default when the prompt
165
+ // is ambiguous.
166
+ return INTENT_UI_IMPROVE;
167
+ }
168
+ // No isolated UI or backend signal — but a full-stack feature build
169
+ // ("build me a feature", "scaffold the billing module") should reach
170
+ // the mixed flow rather than fall through to the backend default.
171
+ if (_is_fullstack_feature(text)) {
172
+ return INTENT_MIXED;
173
+ }
174
+ return INTENT_BACKEND;
175
+ }
176
+
177
+ /**
178
+ * Map an intent label to a directive-set name.
179
+ *
180
+ * Centralised here so the dispatcher and the refine step share one
181
+ * routing table; a future intent (`infra`, `security-review`)
182
+ * only needs a single edit. Unknown labels raise `ValueError` —
183
+ * silently falling back to `backend` would mask classifier bugs.
184
+ */
185
+ export function directive_set_for(intent: string): string {
186
+ if (!KNOWN_INTENTS.has(intent)) {
187
+ throw new ValueError(
188
+ `unknown intent ${pyStrRepr(intent)}; ` +
189
+ `expected one of ${pyListRepr(pySorted(KNOWN_INTENTS))}`,
190
+ );
191
+ }
192
+ if (intent === INTENT_UI_BUILD || intent === INTENT_UI_IMPROVE) {
193
+ return 'ui';
194
+ }
195
+ if (intent === INTENT_UI_TRIVIAL) {
196
+ return 'ui-trivial';
197
+ }
198
+ if (intent === INTENT_MIXED) {
199
+ return 'mixed';
200
+ }
201
+ return 'backend';
202
+ }
203
+
204
+ // --- helpers ----------------------------------------------------------
205
+
206
+ function _has_ui_signal(text: string): boolean {
207
+ for (const w of _UI_NOUNS) {
208
+ if (reSearch(wordBoundary(w), text)) {
209
+ return true;
210
+ }
211
+ }
212
+ for (const s of _UI_STYLE) {
213
+ if (text.includes(s)) {
214
+ return true;
215
+ }
216
+ }
217
+ return false;
218
+ }
219
+
220
+ function _has_backend_signal(text: string): boolean {
221
+ for (const w of _BACKEND_SIGNALS) {
222
+ if (reSearch(wordBoundary(w), text)) {
223
+ return true;
224
+ }
225
+ }
226
+ return false;
227
+ }
228
+
229
+ function _is_trivial(text: string): boolean {
230
+ if (reSearch(_TRIVIAL_PATTERN, text)) {
231
+ return true;
232
+ }
233
+ let hasVerb = false;
234
+ for (const v of _TRIVIAL_VERBS) {
235
+ if (reSearch(wordBoundary(v), text)) {
236
+ hasVerb = true;
237
+ break;
238
+ }
239
+ }
240
+ return hasVerb && pySplit(text).length <= 14;
241
+ }
242
+
243
+ function _is_improve(text: string): boolean {
244
+ if (reSearch(_EXISTING_SURFACE, text)) {
245
+ return true;
246
+ }
247
+ for (const v of _IMPROVE_VERBS) {
248
+ if (reSearch(wordBoundary(v), text)) {
249
+ return true;
250
+ }
251
+ }
252
+ return false;
253
+ }
254
+
255
+ function _is_build(text: string): boolean {
256
+ if (reSearch(_NEW_SURFACE, text)) {
257
+ return true;
258
+ }
259
+ for (const v of _BUILD_VERBS) {
260
+ if (reSearch(wordBoundary(v), text)) {
261
+ return true;
262
+ }
263
+ }
264
+ return false;
265
+ }
266
+
267
+ /**
268
+ * True when the prompt is a full-stack feature build.
269
+ *
270
+ * Either the `full-stack` phrase appears, or a full-stack noun is
271
+ * the object of a build verb ({@link _FULLSTACK_BUILD}). Only reached
272
+ * after the UI and backend rungs miss, so an isolated UI / backend
273
+ * prompt never lands here — this rung exists for the layer-agnostic
274
+ * "build me a feature" shape that would otherwise fall through to the
275
+ * backend default. The build-object adjacency keeps it from firing on
276
+ * incidental noun co-occurrence (e.g. a math "product" in an AC).
277
+ */
278
+ function _is_fullstack_feature(text: string): boolean {
279
+ return (
280
+ reSearch(_FULLSTACK_PHRASE, text) || reSearch(_FULLSTACK_BUILD, text)
281
+ );
282
+ }
283
+
284
+ /**
285
+ * Classify `state.input` and write `intent` + `directive_set` in place.
286
+ *
287
+ * Idempotent and override-safe: if `state.intent` is already a
288
+ * UI-track or mixed label (`ui-build`, `ui-improve`, `ui-trivial`,
289
+ * `mixed`), the routing is left untouched. Only freshly-built states
290
+ * carrying the construction default `backend-coding` are reclassified.
291
+ * Loaded state files round-trip without losing a previously-recorded
292
+ * intent — including a manual user override in the JSON.
293
+ *
294
+ * The text fed to the classifier depends on the input envelope:
295
+ *
296
+ * - `prompt` → `state.input.data["raw"]`
297
+ * - `ticket` → `state.input.data["title"]` + first non-empty
298
+ * acceptance criterion, falling back to `description` when AC is
299
+ * missing. Title is passed separately so single-line ticket
300
+ * headlines (`"Add CSV export"`) classify identically whether
301
+ * they arrive in the body or the title slot.
302
+ * - `diff` / `file` → routed directly to `ui-improve` without
303
+ * running the heuristic. Both envelopes are R3 Phase 1 inputs that
304
+ * describe an existing UI surface ("improve this screen"); the
305
+ * classifier's prose-oriented signals do not apply, and the audit +
306
+ * design directives downstream are the right place to read the
307
+ * diff/file contents.
308
+ */
309
+ export function populate_routing(state: WorkState): void {
310
+ if (state.intent !== INTENT_BACKEND) {
311
+ return;
312
+ }
313
+
314
+ if (state.input.kind === 'diff' || state.input.kind === 'file') {
315
+ state.intent = INTENT_UI_IMPROVE;
316
+ state.directive_set = directive_set_for(INTENT_UI_IMPROVE);
317
+ return;
318
+ }
319
+
320
+ const [text, title] = _extract_text(state);
321
+ const intent = classify_intent(text, { title });
322
+ state.intent = intent;
323
+ state.directive_set = directive_set_for(intent);
324
+ }
325
+
326
+ function _extract_text(state: WorkState): [string, string | null] {
327
+ const data = state.input.data ?? {};
328
+ if (state.input.kind === 'prompt') {
329
+ // Python: str(data.get("raw") or "")
330
+ const raw = 'raw' in data ? data['raw'] : undefined;
331
+ return [pyStr(_pyTruthy(raw) ? raw : ''), null];
332
+ }
333
+ const title = 'title' in data ? data['title'] : undefined;
334
+ const title_str =
335
+ typeof title === 'string' && pyStrip(title) ? pyStr(title) : null;
336
+ const body_parts: string[] = [];
337
+ const ac = 'acceptance_criteria' in data ? data['acceptance_criteria'] : undefined;
338
+ if (Array.isArray(ac)) {
339
+ for (const item of ac) {
340
+ if (typeof item === 'string') {
341
+ body_parts.push(pyStr(item));
342
+ }
343
+ }
344
+ }
345
+ const description = 'description' in data ? data['description'] : undefined;
346
+ if (typeof description === 'string' && pyStrip(description)) {
347
+ body_parts.push(description);
348
+ }
349
+ return [body_parts.join(' '), title_str];
350
+ }
351
+
352
+ // ── Python-parity helpers ───────────────────────────────────────────────
353
+
354
+ /** Raised in place of Python's `ValueError` for the unknown-intent guard. */
355
+ export class ValueError extends Error {
356
+ constructor(message: string) {
357
+ super(message);
358
+ this.name = 'ValueError';
359
+ Object.setPrototypeOf(this, ValueError.prototype);
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Mirror `re.escape(word)` then `re.search(rf"\b{...}\b", text)` as a boolean.
365
+ * The classifier's words are all `[a-z]+` ASCII, so escaping is a no-op for
366
+ * them; escape defensively anyway so the helper is faithful for any input.
367
+ * Patterns are flag-only (no `g`) — `.test` carries no `lastIndex` state.
368
+ */
369
+ function wordBoundary(word: string): RegExp {
370
+ return new RegExp(`\\b${reEscape(word)}\\b`);
371
+ }
372
+
373
+ function reSearch(pattern: RegExp, text: string): boolean {
374
+ return pattern.test(text);
375
+ }
376
+
377
+ /**
378
+ * Mirror Python `re.escape` — escapes every character that is not an ASCII
379
+ * letter, digit, or underscore. CPython 3.7+ escapes only special chars, but
380
+ * for the ASCII-word inputs here the result is identical to the verb itself.
381
+ */
382
+ function reEscape(s: string): string {
383
+ return s.replace(/[^A-Za-z0-9_]/g, (ch) => '\\' + ch);
384
+ }
385
+
386
+ /**
387
+ * Mirror Python `str.split()` (no args) — split on runs of whitespace,
388
+ * dropping leading/trailing empties. `trim().split(/\s+/)` matches; guard the
389
+ * empty string so `"".split(/\s+/)` does not yield `[""]` (Python returns []).
390
+ */
391
+ function pySplit(s: string): string[] {
392
+ const t = s.trim();
393
+ if (t === '') return [];
394
+ return t.split(/\s+/);
395
+ }
396
+
397
+ /** Mirror Python `str.strip()` — `String.trim()` is faithful here. */
398
+ function pyStrip(s: string): string {
399
+ return s.trim();
400
+ }
401
+
402
+ /**
403
+ * Mirror Python `str.lower()`. CPython lower-cases by full Unicode mapping;
404
+ * JS `toLowerCase()` follows the same Unicode default case-folding for the
405
+ * code points these heuristics see (the inputs are dominated by ASCII).
406
+ */
407
+ function pyLower(s: string): string {
408
+ return s.toLowerCase();
409
+ }
410
+
411
+ /**
412
+ * Mirror Python `str(x)` for the values `_extract_text` coerces (str, or the
413
+ * `or ""` fallback which is already a string). Only string-or-empty flows here.
414
+ */
415
+ function pyStr(v: unknown): string {
416
+ return typeof v === 'string' ? v : String(v);
417
+ }
418
+
419
+ /** Mirror Python truthiness for the `data.get("raw") or ""` short-circuit. */
420
+ function _pyTruthy(v: unknown): v is string {
421
+ if (v === null || v === undefined) return false;
422
+ if (typeof v === 'boolean') return v;
423
+ if (typeof v === 'number') return v !== 0;
424
+ if (typeof v === 'string') return v.length > 0;
425
+ if (Array.isArray(v)) return v.length > 0;
426
+ if (typeof v === 'object') return Object.keys(v).length > 0;
427
+ return true;
428
+ }
429
+
430
+ /**
431
+ * Mirror Python `repr(str)` — see the `state.py` twin's `pyStrRepr`. Used for
432
+ * the `{intent!r}` tail in the unknown-intent error.
433
+ */
434
+ function pyStrRepr(s: string): string {
435
+ const hasSingle = s.includes("'");
436
+ const hasDouble = s.includes('"');
437
+ const quote = hasSingle && !hasDouble ? '"' : "'";
438
+ let out = quote;
439
+ for (const ch of s) {
440
+ const code = ch.codePointAt(0) as number;
441
+ if (ch === '\\') out += '\\\\';
442
+ else if (ch === quote) out += '\\' + quote;
443
+ else if (ch === '\n') out += '\\n';
444
+ else if (ch === '\r') out += '\\r';
445
+ else if (ch === '\t') out += '\\t';
446
+ else if (code < 0x20 || code === 0x7f) {
447
+ out += `\\x${code.toString(16).padStart(2, '0')}`;
448
+ } else {
449
+ out += ch;
450
+ }
451
+ }
452
+ return out + quote;
453
+ }
454
+
455
+ /** Python `repr(list_of_str)` for the `sorted(KNOWN_INTENTS)` error tail. */
456
+ function pyListRepr(items: string[]): string {
457
+ return '[' + items.map((s) => pyStrRepr(s)).join(', ') + ']';
458
+ }
459
+
460
+ /**
461
+ * Python `sorted(set_of_str)` — code-point ascending, the default `str`
462
+ * ordering CPython uses.
463
+ */
464
+ function pySorted(values: ReadonlySet<string>): string[] {
465
+ return [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
466
+ }