@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/package.json +4 -1
- package/src/agent-src/README.md +64 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
- package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
- package/src/agent-src/templates/AGENTS.md +34 -0
- package/src/agent-src/templates/scripts/README.md +195 -0
- package/src/agent-src/templates/scripts/check_memory.ts +662 -0
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
- package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
- package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
- package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
- package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
- package/src/agent-src/templates/scripts/memory_report.ts +868 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
- package/src/agent-src/templates/scripts/memory_status.ts +215 -0
- package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
- package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
- package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
- package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
- package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
- package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
- package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
- package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
- package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
- package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
- package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
- package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
- package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
- package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
- package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
- package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
- package/src/install/atomic.ts +111 -0
- package/src/install/conflict.ts +226 -0
- package/src/install/detect.ts +280 -0
- package/src/install/paths.ts +90 -0
- package/src/install/plan.ts +205 -0
- package/src/install/preflight.ts +210 -0
- package/src/install/selectedTools.ts +52 -0
- package/src/install/toolDetection.ts +120 -0
- package/src/install/txlog.ts +161 -0
- package/src/install/types.ts +175 -0
- package/src/install/wizard-plan.ts +216 -0
- 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
|
+
}
|