@fusengine/harness 0.1.28 → 0.1.30

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 (35) hide show
  1. package/dist/adapters/claude/index.mjs +1 -1
  2. package/dist/adapters/cline/index.mjs +1 -1
  3. package/dist/adapters/codex/index.mjs +1 -1
  4. package/dist/adapters/cursor/index.mjs +1 -1
  5. package/dist/adapters/gemini/index.mjs +1 -1
  6. package/dist/cache/index.mjs +2 -2
  7. package/dist/{cache-BzbX-ztL.mjs → cache-C9z9LclL.mjs} +1 -31
  8. package/dist/{claude-phC5Uh_W.mjs → claude-B9FYp0Yw.mjs} +1 -1
  9. package/dist/cli/bin.mjs +14 -4
  10. package/dist/cli/index.mjs +1 -1
  11. package/dist/describe-CPtgUzFS.mjs +1038 -0
  12. package/dist/{evaluate-CFYPF3re.mjs → evaluate-j3gRJ_ng.mjs} +14 -2
  13. package/dist/freshness/index.mjs +1 -1
  14. package/dist/{freshness-CezohJHo.mjs → freshness-otdUpuvP.mjs} +1 -1
  15. package/dist/handle-USWK4NSE.mjs +2300 -0
  16. package/dist/index-mISsk0ff.d.mts +438 -0
  17. package/dist/index.d.mts +2 -2
  18. package/dist/index.mjs +7 -8
  19. package/dist/{json-io-xpTDuvtn.mjs → json-io-CAn72gI4.mjs} +1 -1
  20. package/dist/policy/index.d.mts +2 -2
  21. package/dist/policy/index.mjs +4 -4
  22. package/dist/policy-la_KkjCS.mjs +1 -0
  23. package/dist/{run-B8n-H5hA.mjs → run-CXsV-wIJ.mjs} +1 -1
  24. package/dist/runtime/index.d.mts +454 -8
  25. package/dist/runtime/index.mjs +2 -2
  26. package/dist/state/index.mjs +1 -1
  27. package/dist/{state-Cs0Y0MG_.mjs → state-ByhLeKyD.mjs} +1 -1
  28. package/dist/{store-BnHpq2ZB.mjs → store-D-ge2ZPI.mjs} +1 -1
  29. package/dist/{store-DeIsfMg5.mjs → store-PrNPm6So.mjs} +30 -1
  30. package/dist/tracking/index.mjs +1 -1
  31. package/package.json +10 -3
  32. package/dist/handle-DnOw05K8.mjs +0 -347
  33. package/dist/index-DNAzITvw.d.mts +0 -227
  34. package/dist/policy-EuVJ_5hS.mjs +0 -33
  35. package/dist/verbosity-CXpf3aQQ.mjs +0 -98
@@ -0,0 +1,1038 @@
1
+ import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
2
+ import { k as countLines } from "./evaluate-j3gRJ_ng.mjs";
3
+ import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
4
+ import { t as routeReferences } from "./router-D8cVrI-s.mjs";
5
+ import { join } from "node:path";
6
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ //#region src/policy/detect-project.ts
9
+ /** Keywords that signal a development task (APEX trigger). */
10
+ const DEV_KEYWORDS = /\b(implement|create|build|fix|add|refactor|develop|feature|bug|update|modify|change|write|code)\b/i;
11
+ /** True when the prompt invokes the /apex command. */
12
+ function isApexCommand(prompt) {
13
+ return /(?:^|\s)\/apex|\/fuse-ai-pilot:apex/i.test(prompt);
14
+ }
15
+ /**
16
+ * Detect a project-internal modular architecture (a sub-architecture the
17
+ * framework-level {@link detectProjectType} doesn't capture): Fusengine's
18
+ * FuseCore (Laravel) or a `modules/`-based Next.js layout.
19
+ */
20
+ function detectModularArchitecture(dir) {
21
+ const has = (f) => existsSync(join(dir, f));
22
+ if (has("FuseCore") && has("artisan")) return "fusecore";
23
+ if (has("modules") && (has("next.config.js") || has("next.config.ts") || has("next.config.mjs"))) return "nextjs-modular";
24
+ return null;
25
+ }
26
+ /**
27
+ * Resolve the skill a detected modular architecture forces.
28
+ *
29
+ * Ports the Python `check-nextjs-skill.py` / `check-laravel-skill.py` gates:
30
+ * when the project is detected on disk as a modular architecture, a specific
31
+ * skill is required ('solid-nextjs' for nextjs-modular, 'fusecore' for
32
+ * fusecore). Returns `null` when no modular architecture is detected.
33
+ *
34
+ * @param cwd - Project root directory to scan.
35
+ * @returns The forced skill name, or `null` when none applies.
36
+ */
37
+ function requiredArchSkill(cwd) {
38
+ switch (detectModularArchitecture(cwd)) {
39
+ case "nextjs-modular": return "solid-nextjs";
40
+ case "fusecore": return "fusecore";
41
+ default: return null;
42
+ }
43
+ }
44
+ /** Detect the project type by scanning config files in `dir`. */
45
+ function detectProjectType(dir) {
46
+ const has = (f) => existsSync(join(dir, f));
47
+ if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) return "nextjs";
48
+ if (has("nuxt.config.ts") || has("nuxt.config.js")) return "nuxt";
49
+ if (has("angular.json")) return "angular";
50
+ if (has("svelte.config.js") || has("svelte.config.ts")) return "svelte";
51
+ if (has("vite.config.ts") && has("src/App.vue")) return "vue";
52
+ if (has("vite.config.ts") || has("vite.config.js")) return "react";
53
+ if (has("tailwind.config.js") || has("tailwind.config.ts")) return "tailwind";
54
+ if (has("composer.json") && has("artisan")) return "laravel";
55
+ if (has("Gemfile") && has("config/routes.rb")) return "rails";
56
+ if (has("requirements.txt") || has("pyproject.toml") || has("setup.py")) return has("manage.py") ? "django" : "python";
57
+ if (has("go.mod")) return "go";
58
+ if (has("Cargo.toml")) return "rust";
59
+ if (has("Package.swift")) return "swift";
60
+ if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) return "java";
61
+ if (has("build.sbt")) return "scala";
62
+ if (has("mix.exs")) return "elixir";
63
+ if (has("Gemfile")) return "ruby";
64
+ return "generic";
65
+ }
66
+ //#endregion
67
+ //#region src/policy/apex.ts
68
+ /** Gate: Context7 + Exa must have been consulted this session. */
69
+ const docConsultedGate = (ctx) => isDocConsulted(ctx.authorizations, ctx.sessionId) ? null : {
70
+ kind: "block",
71
+ title: "APEX: documentation not consulted",
72
+ reason: formatDocDeny(ctx.framework),
73
+ actions: ["Call mcp__context7__query-docs", "Call mcp__exa__web_search_exa"]
74
+ };
75
+ /** Gate: the routed SOLID references for this edit must have been read. */
76
+ const solidReadGate = (ctx) => {
77
+ if (!ctx.refs?.length) return null;
78
+ const routed = routeReferences(ctx.refs, ctx.filePath, ctx.content);
79
+ if (!routed) return null;
80
+ const read = new Set(ctx.refsRead ?? []);
81
+ const missing = routed.required.map((r) => r.meta.filePath).filter((p) => !read.has(p));
82
+ if (missing.length === 0) return null;
83
+ return {
84
+ kind: "block",
85
+ title: `APEX: read SOLID references for ${ctx.framework}`,
86
+ reason: `Read these before editing ${ctx.filePath}:`,
87
+ actions: missing
88
+ };
89
+ };
90
+ /** Gate: the required prior agents (explore + research) must have run within the window. */
91
+ const freshnessGate = (ctx) => ctx.agentsFresh === false ? {
92
+ kind: "block",
93
+ title: "APEX: explore + research required",
94
+ reason: `Run explore-codebase and research-expert (within the freshness window) before editing ${ctx.framework}.`,
95
+ actions: ["Launch the explore-codebase agent", "Launch the research-expert agent"]
96
+ } : null;
97
+ /** Gate: brainstorming must precede creating new files when flagged. */
98
+ const brainstormGate = (ctx) => ctx.brainstormRequired && ctx.brainstormFresh === false ? {
99
+ kind: "block",
100
+ title: "APEX: brainstorm first",
101
+ reason: `Creation intent detected — brainstorm before creating new ${ctx.framework} files.`,
102
+ actions: ["Launch the brainstorming agent"]
103
+ } : null;
104
+ /** Default APEX gate chain (brainstorm, freshness, docs, SOLID refs). */
105
+ const APEX_GATES = [
106
+ brainstormGate,
107
+ freshnessGate,
108
+ docConsultedGate,
109
+ solidReadGate
110
+ ];
111
+ /**
112
+ * Run the APEX gates (chain-of-responsibility): the first failing gate's prompt
113
+ * wins; null means every gate passed (allow).
114
+ */
115
+ function evaluateApex(ctx, gates = APEX_GATES) {
116
+ return gates.reduce((hit, gate) => hit ?? gate(ctx), null);
117
+ }
118
+ //#endregion
119
+ //#region src/policy/creation-intent.ts
120
+ const CREATE_RE = /\b(?:create|implement|add|build|new|feature|component|generate|make|develop|scaffold)\b/i;
121
+ const SKIP_RE = /\b(?:fix|bug|debug|update|refactor|rename|move|delete|remove|commit|push|edit|modify|change)\b/i;
122
+ /**
123
+ * True when a prompt expresses creation intent (a new feature/component) and is
124
+ * not a fix/refactor — the signal that brainstorming should precede creation.
125
+ * The harness calls this on UserPromptSubmit, then `recordBrainstormRequired`.
126
+ */
127
+ function detectCreationIntent(prompt) {
128
+ return CREATE_RE.test(prompt) && !SKIP_RE.test(prompt);
129
+ }
130
+ //#endregion
131
+ //#region src/policy/verbosity.ts
132
+ /** Exa MCP tools whose result count + token budget are capped. */
133
+ const EXA_TOOLS = /exa__web_search|exa__get_code_context|exa_web_search|exa_get_code_context/i;
134
+ /** Context7 doc tool whose token budget is capped. */
135
+ const CONTEXT7_TOOLS = /context7__query-docs|context7_query-docs|query-docs/i;
136
+ /** Max results an exa MCP call may request. */
137
+ const MAX_EXA_RESULTS = 3;
138
+ /** Max token budget for exa `tokensNum` / context7 `tokens`. */
139
+ const MAX_TOKENS = 2e3;
140
+ /**
141
+ * Cap an MCP call's verbosity — exa `numResults` ≤ 3 (+ `tokensNum` ≤ 2000),
142
+ * Context7 `tokens` ≤ 2000. Returns the capped input (a mutation for the harness
143
+ * to apply) when a change is needed, else null.
144
+ */
145
+ function capVerbosity(tool, input) {
146
+ const out = { ...input };
147
+ let changed = false;
148
+ const cap = (key, max, force) => {
149
+ const v = out[key];
150
+ if (typeof v === "number" && v > max || force && typeof v !== "number") {
151
+ out[key] = max;
152
+ changed = true;
153
+ }
154
+ };
155
+ if (EXA_TOOLS.test(tool)) {
156
+ cap("numResults", 3, true);
157
+ cap("tokensNum", MAX_TOKENS, false);
158
+ } else if (CONTEXT7_TOOLS.test(tool)) cap("tokens", MAX_TOKENS, false);
159
+ return changed ? out : null;
160
+ }
161
+ //#endregion
162
+ //#region src/policy/framework-solid-exclude.ts
163
+ /** Build-output / dependency paths excluded from React/Next.js SOLID gating. */
164
+ const JS_EXCLUDE_RE = /(node_modules|dist|build|\.next)/;
165
+ /** Vendored dependency paths excluded from Laravel/PHP SOLID gating. */
166
+ const PHP_EXCLUDE_RE = /\/vendor\//;
167
+ /** Derived/build artifact paths excluded from Swift SOLID gating. */
168
+ const SWIFT_EXCLUDE_RE = /(\.build|DerivedData|Pods)/;
169
+ /**
170
+ * Whether a JS/TS (React/Next.js) file path is an excluded build artifact.
171
+ * Matches the Python validators' early-return guard to avoid false positives.
172
+ * @param filePath - absolute path of the file under validation
173
+ */
174
+ function isExcludedJsPath(filePath) {
175
+ return JS_EXCLUDE_RE.test(filePath);
176
+ }
177
+ /**
178
+ * Whether a PHP (Laravel) file path is a vendored dependency to skip.
179
+ * @param filePath - absolute path of the file under validation
180
+ */
181
+ function isExcludedPhpPath(filePath) {
182
+ return PHP_EXCLUDE_RE.test(filePath);
183
+ }
184
+ /**
185
+ * Whether a Swift file path is a derived/build artifact to skip.
186
+ * @param filePath - absolute path of the file under validation
187
+ */
188
+ function isExcludedSwiftPath(filePath) {
189
+ return SWIFT_EXCLUDE_RE.test(filePath);
190
+ }
191
+ //#endregion
192
+ //#region src/policy/framework-solid-gates.ts
193
+ /** Custom hook export (React): `export function/const use[A-Z]`. */
194
+ const HOOK_RE = /^export (function|const) use[A-Z]/m;
195
+ /** Top-level TS interface/type declaration. */
196
+ const TS_DECL_RE = /^(export )?(interface|type) [A-Z]/m;
197
+ /** Client-only React hooks that require the `'use client'` directive. */
198
+ const CLIENT_HOOK_RE = /(useState|useEffect|useRef|onClick|onChange)/;
199
+ /** PHP top-level `interface` declaration. */
200
+ const PHP_INTERFACE_RE = /^interface /m;
201
+ /** Swift top-level `protocol` declaration. */
202
+ const SWIFT_PROTOCOL_RE = /^protocol /m;
203
+ /** Swift type declaration (`class`/`struct`) opening a body. */
204
+ const SWIFT_TYPE_RE = /^(class|struct) [^\n{]* \{/m;
205
+ /** React: line limit, interface separation, custom hooks under `/hooks/`. */
206
+ function reactGate(filePath, content, fileLines) {
207
+ const v = [];
208
+ const max = resolveMaxLines();
209
+ const lines = fileLines ?? countLines(content);
210
+ if (lines > max) v.push(`File has ${lines} lines (limit: ${max}). Split to hooks/, components/, or utils/.`);
211
+ if (filePath.includes("/components/") && TS_DECL_RE.test(content)) v.push("Interface/type in component. Move to src/interfaces/ or src/types/.");
212
+ if (HOOK_RE.test(content) && !filePath.includes("/hooks/")) v.push("Custom hook defined outside hooks/ directory. Move to hooks/.");
213
+ return v;
214
+ }
215
+ /** Next.js: adaptive line limit, interface separation, `'use client'`. */
216
+ function nextGate(filePath, content, fileLines) {
217
+ const v = [];
218
+ const max = /(page|layout|loading|error|not-found)\.(tsx|ts)$/.test(filePath) ? 150 : 100;
219
+ const lines = fileLines ?? countLines(content);
220
+ if (lines > max) v.push(`File has ${lines} lines (limit: ${max}). Split to lib/, hooks/, or components/.`);
221
+ if (/\/(app|components|modules)\//.test(filePath) && !filePath.includes("/interfaces/") && TS_DECL_RE.test(content)) v.push("Interface/type in component. Move to modules/[feature]/src/interfaces/.");
222
+ if (CLIENT_HOOK_RE.test(content)) {
223
+ const head = content.split("\n").slice(0, 5).join("\n");
224
+ if (!head.includes("'use client'") && !head.includes("\"use client\"")) v.push("Client hooks detected but 'use client' directive missing at top.");
225
+ }
226
+ return v;
227
+ }
228
+ /** Laravel/PHP: line limit, interface outside `/Contracts/`, fat controller (>80). */
229
+ function laravelGate(filePath, content, fileLines) {
230
+ const v = [];
231
+ const lines = fileLines ?? countLines(content);
232
+ const max = resolveMaxLines();
233
+ if (lines > max) v.push(`File has ${lines} lines (limit: ${max}). Split using Services, Actions, or Traits.`);
234
+ if (PHP_INTERFACE_RE.test(content) && !filePath.includes("/Contracts/")) v.push("Interface defined outside Contracts/. Move to app/Contracts/ or FuseCore/{Module}/App/Contracts/.");
235
+ if (filePath.includes("/Controllers/") && lines > 80) v.push(`Fat controller (${lines} lines). Extract logic to Services or Actions.`);
236
+ return v;
237
+ }
238
+ /** Swift: adaptive limit, protocol separation, @MainActor, Sendable. */
239
+ function swiftGate(filePath, content, fileLines) {
240
+ const v = [];
241
+ const max = /(View|Screen)\.swift$/.test(filePath) ? 150 : 100;
242
+ const lines = fileLines ?? countLines(content);
243
+ if (lines > max) v.push(`File has ${lines} lines (limit: ${max}). Extract to ViewModels, Services, or subviews.`);
244
+ if (SWIFT_PROTOCOL_RE.test(content) && !filePath.includes("/Protocols/")) v.push("Protocol defined outside Protocols/ directory.");
245
+ if (filePath.endsWith("ViewModel.swift") && !content.includes("@MainActor")) v.push("ViewModel missing @MainActor annotation.");
246
+ if (SWIFT_TYPE_RE.test(content) && content.includes("async ") && !content.includes("Sendable")) v.push("Type uses async but doesn't conform to Sendable.");
247
+ return v;
248
+ }
249
+ //#endregion
250
+ //#region src/policy/framework-solid.ts
251
+ /** Next.js detection: directive, runtime types, or a `next` import. */
252
+ const NEXT_RE = /(use client|use server|NextRequest|NextResponse|from ['"]next)/;
253
+ /** Build a SOLID `block` prompt from one or more violation messages. */
254
+ function block(filePath, violations) {
255
+ return {
256
+ kind: "block",
257
+ title: "SOLID violation",
258
+ reason: `SOLID VIOLATION in ${filePath}: ${violations.join(" ")}`,
259
+ actions: violations
260
+ };
261
+ }
262
+ /** Run the JS/TS gate (React or Next.js), honoring build-artifact exclusions. */
263
+ function jsViolations(filePath, content, fileLines) {
264
+ if (isExcludedJsPath(filePath)) return [];
265
+ return NEXT_RE.test(content) ? nextGate(filePath, content, fileLines) : reactGate(filePath, content, fileLines);
266
+ }
267
+ /**
268
+ * Framework-specific SOLID gate. Dispatches by extension/path to the matching
269
+ * validator (React, Next.js, Laravel, Swift) and returns a blocking
270
+ * {@link Prompt} when any BLOCKING rule fires, or `null` when clean. Excluded
271
+ * build/dependency paths (node_modules, dist, build, .next, vendor, .build,
272
+ * DerivedData, Pods) early-return `null` to avoid false positives.
273
+ * @param filePath - absolute path of the file being written/edited
274
+ * @param content - the file (or new) content under validation
275
+ * @param fileLines - full on-disk line count (set on Edit so a partial
276
+ * `new_string` snippet still judges the whole file, mirroring the base
277
+ * file-size guard / Python `get_full_file_content`). Omit on Write.
278
+ */
279
+ function frameworkSolidGate(filePath, content, fileLines) {
280
+ if (!filePath || !content) return null;
281
+ let violations = [];
282
+ if (filePath.endsWith(".php")) {
283
+ if (isExcludedPhpPath(filePath)) return null;
284
+ violations = laravelGate(filePath, content, fileLines);
285
+ } else if (filePath.endsWith(".swift")) {
286
+ if (isExcludedSwiftPath(filePath)) return null;
287
+ violations = swiftGate(filePath, content, fileLines);
288
+ } else if (/\.(tsx|ts|jsx|js)$/.test(filePath)) violations = jsViolations(filePath, content, fileLines);
289
+ return violations.length ? block(filePath, violations) : null;
290
+ }
291
+ //#endregion
292
+ //#region src/policy/skill-patterns/shadcn.ts
293
+ /**
294
+ * shadcn/ui HTML-to-component detection patterns, ported verbatim from the
295
+ * shared `shadcn_patterns.py` (FORM/OVERLAY/DATA/NAV/LAYOUT/FEEDBACK groups).
296
+ * 32 patterns total (3+6+8+5+5+5). Matched case-insensitively (source `re.IGNORECASE`).
297
+ */
298
+ /** Forms: Button, Input, Textarea, Select, Checkbox, Radio, Switch, etc. */
299
+ const FORM = [
300
+ "<(button|input|select|textarea|label|option|optgroup)\\b",
301
+ "type=\"(checkbox|radio|range|file)\"",
302
+ "<input[^>]*maxLength=\"[1-2]\""
303
+ ];
304
+ /** Overlay: Dialog, AlertDialog, Sheet, Popover, Tooltip, ContextMenu, etc. */
305
+ const OVERLAY = [
306
+ "<dialog\\b",
307
+ "role=\"(dialog|alertdialog)\"",
308
+ "(aria-haspopup|aria-expanded|aria-pressed)=\"",
309
+ "(onContextMenu|onMouseEnter.*onMouseLeave)\\b",
310
+ "\\b(confirm|window\\.confirm)\\(",
311
+ "title=\"[^\"]{2,}\""
312
+ ];
313
+ /** Data: Table, Card, Badge, Avatar, Calendar, Chart, Carousel, Pagination. */
314
+ const DATA = [
315
+ "<(table|thead|tbody|tfoot|th|td|tr|caption|colgroup)\\b",
316
+ "<(article|section)\\b[^>]*className",
317
+ "rounded-full[^>]*className|className[^>]*rounded-full",
318
+ "<img\\b[^>]*rounded",
319
+ "(new Date|\\.toLocaleDateString|date-fns|dayjs)\\b",
320
+ "(recharts|chart\\.js|<svg[^>]*viewBox)",
321
+ "(scroll-snap|embla-carousel|useEmbla)\\b",
322
+ "(page=|currentPage|totalPages|pageSize)\\b"
323
+ ];
324
+ /** Navigation: Breadcrumb, NavigationMenu, Menubar, Sidebar, Tabs. */
325
+ const NAV = [
326
+ "<(nav|aside)\\b",
327
+ "<(menu|menuitem)\\b",
328
+ "role=\"(menubar|menu|menuitem|tablist|tab|tabpanel)\"",
329
+ "aria-label=\"(breadcrumb|navigation|sidebar)\"",
330
+ "aria-current=\"(page|step)\""
331
+ ];
332
+ /** Layout: Accordion, Collapsible, Separator, ScrollArea, AspectRatio. */
333
+ const LAYOUT = [
334
+ "<(hr|details|summary)\\b",
335
+ "role=\"separator\"",
336
+ "overflow-(auto|scroll|y-auto|x-auto)",
337
+ "(aspect-ratio|aspect-video|aspect-square)\\b",
338
+ "(resize|cursor-(col|row)-resize)\\b"
339
+ ];
340
+ /** Feedback: Alert, Toast, Progress, Skeleton, Spinner. */
341
+ const FEEDBACK = [
342
+ "role=\"(alert|status|progressbar)\"",
343
+ "aria-live=\"(polite|assertive)\"",
344
+ "<progress\\b",
345
+ "(animate-pulse|animate-spin)\\b",
346
+ "(sonner|react-hot-toast|\\.toast\\()\\b"
347
+ ];
348
+ /** All 32 shadcn detection patterns combined. */
349
+ const SHADCN = [
350
+ ...FORM,
351
+ ...OVERLAY,
352
+ ...DATA,
353
+ ...NAV,
354
+ ...LAYOUT,
355
+ ...FEEDBACK
356
+ ];
357
+ //#endregion
358
+ //#region src/policy/skill-patterns/react.ts
359
+ /**
360
+ * React skill-trigger patterns, ported verbatim from `react_skill_triggers.py`.
361
+ * Matched case-insensitively (source `re.IGNORECASE`).
362
+ */
363
+ /** Map of React sub-skill name → triggering code patterns. */
364
+ const REACT_TRIGGERS = {
365
+ "react-19": [
366
+ "\\buse\\b\\s*\\(",
367
+ "useOptimistic\\b",
368
+ "useActionState\\b",
369
+ "useEffectEvent\\b",
370
+ "<Activity\\b",
371
+ "from\\s+['\"]react['\"]"
372
+ ],
373
+ "react-tanstack-router": [
374
+ "(createRouter|createRoute|createRootRoute)\\b",
375
+ "(useNavigate|useParams|useSearch|useLoaderData)\\b",
376
+ "from\\s+['\"]@tanstack/(react-router|router)",
377
+ "(routeTree|createFileRoute|createLazyFileRoute)\\b"
378
+ ],
379
+ "react-forms": [
380
+ "(useForm|useAppForm|createFormHook|formOptions)\\b",
381
+ "(mergeForm|formApi|FieldApi|FormApi)\\b",
382
+ "form\\.(Field|Subscribe|handleSubmit)\\b",
383
+ "from\\s+['\"]@tanstack/(react-form|zod-form-adapter)"
384
+ ],
385
+ "react-state": [
386
+ "(create|createStore)\\(\\s*\\(\\s*set",
387
+ "from\\s+['\"]zustand(/\\w+)?\"",
388
+ "(useShallow|useStore|skipHydration)\\b",
389
+ "(persist|devtools|immer)\\("
390
+ ],
391
+ "react-testing": [
392
+ "(render|screen|fireEvent|waitFor)\\b",
393
+ "from\\s+['\"]@testing-library/react",
394
+ "(describe|it|expect|vi\\.|jest\\.)\\b",
395
+ "from\\s+['\"]vitest"
396
+ ],
397
+ "react-shadcn": SHADCN,
398
+ "react-i18n": [
399
+ "(useTranslation|Trans)\\b",
400
+ "from\\s+['\"]react-i18next",
401
+ "\\bt\\(\\s*['\"]",
402
+ "i18n\\.(language|changeLanguage)"
403
+ ]
404
+ };
405
+ //#endregion
406
+ //#region src/policy/skill-patterns/nextjs.ts
407
+ /**
408
+ * Next.js skill-trigger patterns, ported verbatim from `nextjs_skill_triggers.py`.
409
+ * Matched case-insensitively (source `re.IGNORECASE`).
410
+ */
411
+ /** Map of Next.js sub-skill name → triggering code patterns. */
412
+ const NEXTJS_TRIGGERS = {
413
+ "better-auth": [
414
+ "(authClient|betterAuth|createAuthClient)\\b",
415
+ "(signIn|signUp|signOut|useSession|getSession)\\b",
416
+ "auth\\.(api|handler)\\b",
417
+ "(prismaAdapter|drizzleAdapter|mongodbAdapter)\\b",
418
+ "(twoFactor|passkey|magicLink|emailOtp|organization)\\b",
419
+ "(apiKey|bearer|jwt|sso|scim|captcha|anonymous)\\b",
420
+ "from\\s+['\"].*better-auth"
421
+ ],
422
+ "nextjs-tanstack-form": [
423
+ "(useForm|useAppForm|createFormHook|formOptions)\\b",
424
+ "(mergeForm|formApi|FieldApi|FormApi)\\b",
425
+ "form\\.(Field|Subscribe|handleSubmit)\\b",
426
+ "(zodValidator|onServerValidate)\\b",
427
+ "from\\s+['\"]@tanstack/(react-form|zod-form-adapter)"
428
+ ],
429
+ "prisma-7": [
430
+ "(PrismaClient|prismaAdapter)\\b",
431
+ "prisma\\.(\\w+\\.\\w+|\\$\\w+)",
432
+ "(globalForPrisma|\\$transaction|\\$queryRaw|\\$executeRaw)\\b",
433
+ "from\\s+['\"](@prisma|\\..*generated.*prisma)"
434
+ ],
435
+ "nextjs-shadcn": SHADCN,
436
+ "nextjs-zustand": [
437
+ "(create|createStore)\\(\\s*\\(\\s*set",
438
+ "from\\s+['\"]zustand(/\\w+)?\"",
439
+ "(useShallow|useStore|skipHydration)\\b",
440
+ "\\.(getState|setState|subscribe)\\(\\)",
441
+ "(persist|devtools|immer)\\("
442
+ ],
443
+ "nextjs-i18n": [
444
+ "(useTranslations|useLocale|useMessages|useFormatter)\\b",
445
+ "(getTranslations|getLocale|getMessages|getFormatter)\\b",
446
+ "(NextIntlClientProvider|defineRouting)\\b",
447
+ "from\\s+['\"]next-intl(/\\w+)?\"",
448
+ "\\bt\\(\\s*['\"]"
449
+ ]
450
+ };
451
+ //#endregion
452
+ //#region src/policy/skill-patterns/laravel.ts
453
+ /**
454
+ * Laravel skill-trigger patterns, ported verbatim from `laravel_patterns.py`
455
+ * (consumed by `laravel_skill_triggers.py`). Matched case-insensitively.
456
+ */
457
+ /** Map of Laravel sub-skill name → triggering PHP code patterns. */
458
+ const LARAVEL_TRIGGERS = {
459
+ "fusecore": [
460
+ "FuseCore\\\\[A-Za-z]+\\\\App\\\\",
461
+ "use HasModule\\b",
462
+ "ModuleServiceProvider\\b",
463
+ "ModuleInterface\\b"
464
+ ],
465
+ "laravel-eloquent": [
466
+ "(extends Model|HasFactory|belongsTo|hasMany|hasOne|morphTo)\\b",
467
+ "\\$this->belongsToMany|->with\\(|->whereHas\\(",
468
+ "(Eloquent|Model)::(find|where|create|update|all)\\b"
469
+ ],
470
+ "laravel-api": [
471
+ "(JsonResource|ResourceCollection|apiResource)\\b",
472
+ "Route::(get|post|put|delete|apiResource)\\(",
473
+ "(response\\(\\)->json|Request \\$request)\\b"
474
+ ],
475
+ "laravel-auth": [
476
+ "(Auth::|auth\\(\\)|Sanctum|Passport|Socialite)\\b",
477
+ "(Gate::|Policy|can\\(|authorize)\\b",
478
+ "(middleware\\(['\"]auth|LoginController|RegisterController)\\b"
479
+ ],
480
+ "laravel-livewire": [
481
+ "(extends Component|Livewire|wire:|#\\[On)\\b",
482
+ "(mount|render|emit|dispatch)\\(\\)",
483
+ "@livewire|<livewire:"
484
+ ],
485
+ "laravel-queues": [
486
+ "(implements ShouldQueue|dispatch\\(|Bus::)\\b",
487
+ "(Queue::|Job|Batch|Chain)\\b",
488
+ "(onQueue|onConnection|tries|backoff)\\b"
489
+ ],
490
+ "laravel-billing": ["(Billable|subscription|Cashier)\\b", "(createSubscription|newSubscription|charge)\\("],
491
+ "laravel-stripe-connect": [
492
+ "(StripeConnect|connectAccount|onboardingUrl)\\b",
493
+ "(paymentIntent|transfer|payout|splitPayment)\\b",
494
+ "Stripe\\\\\\\\(Account|Transfer|PaymentIntent)\\b"
495
+ ],
496
+ "laravel-testing": [
497
+ "(extends TestCase|RefreshDatabase|WithFaker)\\b",
498
+ "(assertStatus|assertJson|assertSee|assertRedirect)\\(",
499
+ "(factory\\(|Pest|it\\(|test\\(|expect\\()\\b"
500
+ ],
501
+ "laravel-migrations": [
502
+ "(Schema::|Blueprint|->table|->create)\\b",
503
+ "(->string|->integer|->boolean|->foreignId|->index)\\(",
504
+ "extends Migration\\b"
505
+ ],
506
+ "laravel-blade": [
507
+ "(@extends|@section|@yield|@component|@slot)\\b",
508
+ "(@if|@foreach|@include|@push|@stack)\\b",
509
+ "(Blade::|x-[a-z])\\b"
510
+ ],
511
+ "laravel-permission": [
512
+ "(hasRole|givePermissionTo|assignRole|spatie)\\b",
513
+ "(Permission|Role)::(create|findByName)\\b",
514
+ "@can\\b|@role\\b|middleware.*role:"
515
+ ],
516
+ "laravel-i18n": ["(__\\(|trans\\(|trans_choice\\(|@lang)\\b", "Lang::|->locale\\(|setLocale\\b"],
517
+ "laravel-vite": ["(@vite|@viteReactRefresh|Vite::)\\b"]
518
+ };
519
+ //#endregion
520
+ //#region src/policy/skill-patterns/swift.ts
521
+ /**
522
+ * Swift / Apple skill-trigger patterns, ported verbatim from
523
+ * `swift_skill_triggers.py`. 9 skills.
524
+ *
525
+ * NOTE: Swift uses case-SENSITIVE matching (source `re.search` WITHOUT
526
+ * `re.IGNORECASE`). The framework name is registered in
527
+ * `CASE_SENSITIVE_FRAMEWORKS` so the gate compiles these without the `i` flag.
528
+ */
529
+ /** Map of Swift sub-skill name → triggering code patterns (case-sensitive). */
530
+ const SWIFT_TRIGGERS = {
531
+ "swiftui-core": [
532
+ "\\bstruct\\s+\\w+\\s*:\\s*View\\b",
533
+ "@State\\b",
534
+ "@Binding\\b",
535
+ "@Observable\\b",
536
+ "@Environment\\b",
537
+ "NavigationStack\\b",
538
+ "\\.sheet\\b",
539
+ "\\.toolbar\\b",
540
+ "\\.task\\b"
541
+ ],
542
+ "swift-core": [
543
+ "\\bactor\\b",
544
+ "\\basync\\s+(let|func|throws)\\b",
545
+ "\\bawait\\b",
546
+ "Task\\s*\\{",
547
+ "TaskGroup\\b",
548
+ "Sendable\\b",
549
+ "@MainActor\\b"
550
+ ],
551
+ "ios": [
552
+ "UIKit|UIViewController|UIView\\b",
553
+ "UIApplication\\b",
554
+ "\\.simulatorId\\b",
555
+ "import\\s+UIKit\\b"
556
+ ],
557
+ "macos": [
558
+ "AppKit|NSViewController|NSWindow\\b",
559
+ "NSApplication\\b",
560
+ "\\.menuBar\\b",
561
+ "import\\s+AppKit\\b"
562
+ ],
563
+ "watchos": [
564
+ "WatchKit|WKInterface|WKExtension\\b",
565
+ "HealthKit|HKWorkout\\b",
566
+ "WatchConnectivity\\b"
567
+ ],
568
+ "visionos": [
569
+ "RealityKit|RealityView|ImmersiveSpace\\b",
570
+ "\\.volumeBaseplateVisibility\\b",
571
+ "SpatialTapGesture\\b"
572
+ ],
573
+ "ipados": [
574
+ "UISplitViewController|UIKeyCommand\\b",
575
+ "\\.horizontalSizeClass\\b",
576
+ "pencilInteraction\\b"
577
+ ],
578
+ "tvos": [
579
+ "TVUIKit|focusable\\b",
580
+ "\\.focusSection\\b",
581
+ "TVMonogram\\b"
582
+ ],
583
+ "build-distribution": [
584
+ "TestFlight|AppStore\\b",
585
+ "\\.entitlements\\b",
586
+ "codesign|notarize|archive\\b"
587
+ ]
588
+ };
589
+ //#endregion
590
+ //#region src/policy/skill-trigger-patterns.ts
591
+ /**
592
+ * Per-framework code-pattern → required sub-skill data, ported verbatim from the
593
+ * fusengine `*_skill_triggers.py` + `*_patterns.py`
594
+ * (react/nextjs/laravel/swift) and the shared `shadcn_patterns.py`.
595
+ *
596
+ * Pattern groups live in `./skill-patterns/*`. Most frameworks match
597
+ * case-insensitively (source `re.IGNORECASE`); `swift` matches case-SENSITIVELY
598
+ * (source `re.search` without the flag) — see {@link CASE_SENSITIVE_FRAMEWORKS}.
599
+ */
600
+ /**
601
+ * Frameworks whose Python source omits `re.IGNORECASE`, so their regexes must
602
+ * be compiled WITHOUT the `i` flag to stay faithful.
603
+ */
604
+ const CASE_SENSITIVE_FRAMEWORKS = /* @__PURE__ */ new Set(["swift"]);
605
+ /** Map of required sub-skill name → triggering code patterns, keyed by framework. */
606
+ const SKILL_TRIGGERS = {
607
+ react: REACT_TRIGGERS,
608
+ nextjs: NEXTJS_TRIGGERS,
609
+ laravel: LARAVEL_TRIGGERS,
610
+ swift: SWIFT_TRIGGERS
611
+ };
612
+ //#endregion
613
+ //#region src/policy/shadcn-project.ts
614
+ /** Directories whose presence indicates shadcn/ui is installed. */
615
+ const UI_DIRS = [
616
+ "src/components/ui",
617
+ "components/ui",
618
+ "src/modules/cores/shadcn/components/ui"
619
+ ];
620
+ /**
621
+ * Whether `cwd` is a shadcn/ui project, ported from the shared Python
622
+ * `is_shadcn_project` (`shadcn_patterns.py`): true when a `components.json`
623
+ * file exists, or any known `components/ui` directory exists under the root.
624
+ * Used to skip `*-shadcn` sub-skill requirements when shadcn is not installed.
625
+ * @param cwd - project root directory to scan.
626
+ * @returns `true` when shadcn/ui is detected on disk.
627
+ */
628
+ function isShadcnProject(cwd) {
629
+ if (existsSync(join(cwd, "components.json"))) return true;
630
+ return UI_DIRS.some((dir) => existsSync(join(cwd, dir)));
631
+ }
632
+ //#endregion
633
+ //#region src/policy/skill-triggers.ts
634
+ /**
635
+ * Detect which sub-skills the written `content` requires for a `framework`.
636
+ * Faithful to the Python `detect_required_skills`: first matching pattern per
637
+ * skill wins. Most frameworks match case-insensitively (source `re.IGNORECASE`);
638
+ * `swift` matches case-sensitively (see {@link CASE_SENSITIVE_FRAMEWORKS}).
639
+ * @param framework - "react" | "nextjs" | "laravel" | "swift".
640
+ * @param content - the code being written.
641
+ * @returns required sub-skill names (empty when framework unknown / no match).
642
+ */
643
+ function detectRequiredSkills(framework, content) {
644
+ const groups = SKILL_TRIGGERS[framework];
645
+ if (!groups) return [];
646
+ const flags = CASE_SENSITIVE_FRAMEWORKS.has(framework) ? "" : "i";
647
+ const required = [];
648
+ for (const [skill, patterns] of Object.entries(groups)) if (patterns.some((p) => new RegExp(p, flags).test(content))) required.push(skill);
649
+ return required;
650
+ }
651
+ /**
652
+ * Block when a required sub-skill's `skills/<name>/` path is absent from
653
+ * `refsRead`. Mirrors `specific_skill_consulted`, which confirms a skill was
654
+ * read by checking the tracking file contains `skills/<name>/`.
655
+ * @param framework - "react" | "nextjs" | "laravel".
656
+ * @param content - the code being written.
657
+ * @param refsRead - in-session read reference paths.
658
+ * @param forcedSkill - a skill the detected modular architecture forces (optional).
659
+ * @param cwd - project root; when set and not a shadcn project, `*-shadcn`
660
+ * requirements are skipped (ports the Python `is_shadcn_project` filter).
661
+ * @returns a `block` Prompt naming the missing sub-skills, or `null` when satisfied.
662
+ */
663
+ function skillTriggerGate(framework, content, refsRead, forcedSkill, cwd) {
664
+ let required = detectRequiredSkills(framework, content);
665
+ if (forcedSkill && !required.includes(forcedSkill)) required.push(forcedSkill);
666
+ if (cwd && !isShadcnProject(cwd)) required = required.filter((s) => !s.endsWith("-shadcn"));
667
+ const missing = required.filter((s) => !refsRead.some((r) => r.includes(`skills/${s}/`)));
668
+ if (missing.length === 0) return null;
669
+ return {
670
+ kind: "block",
671
+ title: "Required sub-skill not consulted",
672
+ reason: `${framework}: code uses APIs covered by ${missing.join(", ")} but its skill reference was not read this session.`,
673
+ actions: missing.map((s) => `Read skills/${s}/ before writing this code`)
674
+ };
675
+ }
676
+ //#endregion
677
+ //#region src/policy/claude-md-context.ts
678
+ /** Dev-verb regex (FR/EN) that triggers the APEX preamble (case-insensitive). */
679
+ const DEV_VERBS = /(cr[ée]er|impl[ée]menter|ajouter|d[ée]velopper|construire|build|refactor|migrer|implement|create|add|develop)/i;
680
+ /**
681
+ * Detect the project type from the cwd, reproducing the legacy Python logic:
682
+ * package.json containing "next" → nextjs, else "react" → react; else
683
+ * composer.json+artisan → laravel; else Package.swift / *.xcodeproj → swift;
684
+ * else generic.
685
+ * @param cwd - Project root to scan.
686
+ * @returns The detected project type label.
687
+ */
688
+ function detectClaudeMdProjectType(cwd) {
689
+ const pkg = join(cwd, "package.json");
690
+ if (existsSync(pkg)) try {
691
+ const content = readFileSync(pkg, "utf-8");
692
+ if (content.includes("next")) return "nextjs";
693
+ if (content.includes("react")) return "react";
694
+ } catch {}
695
+ if (existsSync(join(cwd, "composer.json")) && existsSync(join(cwd, "artisan"))) return "laravel";
696
+ if (existsSync(join(cwd, "Package.swift"))) return "swift";
697
+ try {
698
+ if (readdirSync(cwd).some((f) => f.endsWith(".xcodeproj"))) return "swift";
699
+ } catch {}
700
+ return "generic";
701
+ }
702
+ /**
703
+ * Build the APEX instruction preamble for a development task.
704
+ * @param projectType - Detected project type label.
705
+ * @param maxLines - SOLID per-file line ceiling.
706
+ * @returns The APEX instruction text.
707
+ */
708
+ function buildApexInstruction(projectType, maxLines) {
709
+ return `INSTRUCTION: This is a development task. Use APEX methodology:
710
+
711
+ **TRACKING FILE**: [project]/.claude/apex/task.json
712
+
713
+ 1. **ANALYZE** (3 AGENTS IN PARALLEL):
714
+ - explore-codebase + research-expert + general-purpose
715
+ - Project type: ${projectType}\n\n2. **PLAN**: TaskCreate (<${maxLines} lines per file)\n\n3. **EXECUTE**: ${projectType}-expert, SOLID principles\n\n4. **EXAMINE**: Run sniper agent after ANY modification`;
716
+ }
717
+ /**
718
+ * Build the UserPromptSubmit injection text: read `~/.claude/CLAUDE.md` and,
719
+ * when the prompt matches a dev verb, prepend the APEX instruction. Returns
720
+ * `null` when CLAUDE.md is absent/unreadable (the hook then emits nothing).
721
+ * @param prompt - The raw user prompt.
722
+ * @param cwd - Project root (for project-type detection).
723
+ * @returns The injection text, or `null` to emit nothing.
724
+ */
725
+ function buildClaudeMdContext(prompt, cwd) {
726
+ const claudeMd = join(homedir(), ".claude", "CLAUDE.md");
727
+ if (!existsSync(claudeMd)) return null;
728
+ let claudeContent;
729
+ try {
730
+ claudeContent = readFileSync(claudeMd, "utf-8");
731
+ } catch {
732
+ return null;
733
+ }
734
+ if (!DEV_VERBS.test(prompt)) return `# CLAUDE.md\n${claudeContent}`;
735
+ return `${buildApexInstruction(detectClaudeMdProjectType(cwd), resolveMaxLines())}\n\n# CLAUDE.md\n${claudeContent}`;
736
+ }
737
+ //#endregion
738
+ //#region src/policy/apex-task-context.ts
739
+ /**
740
+ * Read the current task state from `task.json`, reproducing the legacy Python
741
+ * logic. Any read/parse error falls back to `("1", "", "analyze", "none")`.
742
+ * @param taskFile - Absolute path to `.claude/apex/task.json`.
743
+ * @returns The parsed {@link ApexTaskState}.
744
+ */
745
+ function loadApexTaskState(taskFile) {
746
+ try {
747
+ const data = JSON.parse(readFileSync(taskFile, "utf-8"));
748
+ const id = String(data.current_task ?? "1");
749
+ const task = (data.tasks ?? {})[id] ?? {};
750
+ const subject = typeof task.subject === "string" ? task.subject : "";
751
+ const phase = typeof task.phase === "string" ? task.phase : "analyze";
752
+ const consultedMap = task.doc_consulted ?? {};
753
+ return {
754
+ id,
755
+ subject,
756
+ phase,
757
+ docs: Object.entries(consultedMap).filter(([, v]) => typeof v === "object" && v !== null && v.consulted === true).map(([k]) => k).join(", ") || "none"
758
+ };
759
+ } catch {
760
+ return {
761
+ id: "1",
762
+ subject: "",
763
+ phase: "analyze",
764
+ docs: "none"
765
+ };
766
+ }
767
+ }
768
+ /**
769
+ * Build the APEX context string injected into a Task sub-agent prompt.
770
+ * @param state - The parsed task state.
771
+ * @param maxLines - SOLID per-file line ceiling.
772
+ * @returns The injection text.
773
+ */
774
+ function buildApexTaskContext(state, maxLines) {
775
+ return `⚠️ APEX MODE - Read .claude/apex/AGENTS.md for rules\n\nCurrent: Task #${state.id} - ${state.subject} (Phase: ${state.phase})\nDocs consulted: ${state.docs}\n\nAgent must:\n1. Read task.json → find last 3 completed tasks\n2. Read their notes in docs/ (task-{ID}-{subject}.md)\n3. TaskList → see pending tasks\n4. TaskUpdate(in_progress) → before starting\n5. Apply SOLID (files < ${maxLines} lines)\n6. Write notes to docs/task-{ID}-{subject}.md\n7. TaskUpdate(completed) → triggers auto-commit`;
776
+ }
777
+ /**
778
+ * Build the PreToolUse Task injection, gated on the existence of the project's
779
+ * `.claude/apex/` directory. Returns `null` when APEX is not active (no dir).
780
+ * @param projectRoot - `CLAUDE_PROJECT_DIR` or cwd.
781
+ * @returns The injection text, or `null` to emit nothing.
782
+ */
783
+ function buildApexTaskInjection(projectRoot) {
784
+ const apexDir = join(projectRoot, ".claude", "apex");
785
+ if (!existsSync(apexDir)) return null;
786
+ return buildApexTaskContext(loadApexTaskState(join(apexDir, "task.json")), resolveMaxLines());
787
+ }
788
+ //#endregion
789
+ //#region src/policy/cartographer/indicators.ts
790
+ /**
791
+ * Cartographer indicators — pure data sets used to detect a project root and to
792
+ * exclude noise directories when walking a tree. Ports the constant tables from
793
+ * `generate_project_map.py` / `write_recursive.py`.
794
+ */
795
+ /** Filenames whose presence marks a directory as a project root. */
796
+ const PROJECT_INDICATORS = /* @__PURE__ */ new Set([
797
+ "package.json",
798
+ "deno.json",
799
+ "bun.lockb",
800
+ "bun.lock",
801
+ "tsconfig.json",
802
+ "package-lock.json",
803
+ "yarn.lock",
804
+ "pnpm-lock.yaml",
805
+ "pnpm-workspace.yaml",
806
+ "composer.json",
807
+ "artisan",
808
+ "Cargo.toml",
809
+ "rust-toolchain.toml",
810
+ "go.mod",
811
+ "pyproject.toml",
812
+ "setup.py",
813
+ "setup.cfg",
814
+ "Pipfile",
815
+ "requirements.txt",
816
+ "environment.yml",
817
+ "Gemfile",
818
+ "Package.swift",
819
+ "Podfile",
820
+ "pubspec.yaml",
821
+ "pom.xml",
822
+ "build.gradle",
823
+ "build.gradle.kts",
824
+ "settings.gradle",
825
+ "build.sbt",
826
+ "Makefile",
827
+ "CMakeLists.txt",
828
+ "meson.build",
829
+ "configure.ac",
830
+ "Directory.Build.props",
831
+ "global.json",
832
+ "mix.exs",
833
+ "rebar.config",
834
+ "project.clj",
835
+ "deps.edn",
836
+ "stack.yaml",
837
+ "cabal.project",
838
+ "dune-project",
839
+ "build.zig",
840
+ "gleam.toml",
841
+ "v.mod",
842
+ "Project.toml",
843
+ "DESCRIPTION",
844
+ "cpanfile",
845
+ "Makefile.PL",
846
+ ".luacheckrc",
847
+ "astro.config.mjs",
848
+ "next.config.js",
849
+ "next.config.mjs",
850
+ "nuxt.config.ts",
851
+ "vite.config.ts",
852
+ "next.config.ts",
853
+ "angular.json",
854
+ "svelte.config.js",
855
+ "svelte.config.ts",
856
+ "main.tf",
857
+ "ansible.cfg",
858
+ "pulumi.yaml",
859
+ "cdk.json",
860
+ "Chart.yaml",
861
+ "wrangler.toml",
862
+ "fly.toml",
863
+ "turbo.json",
864
+ "nx.json",
865
+ "BUILD",
866
+ "WORKSPACE",
867
+ "Justfile",
868
+ "Taskfile.yml",
869
+ "docker-compose.yml",
870
+ "docker-compose.yaml",
871
+ "compose.yml",
872
+ "compose.yaml",
873
+ "Dockerfile",
874
+ ".git"
875
+ ]);
876
+ /** Directory names skipped entirely during the tree walk. */
877
+ const EXCLUDE_DIRS = /* @__PURE__ */ new Set([
878
+ "node_modules",
879
+ ".git",
880
+ ".next",
881
+ ".nuxt",
882
+ "dist",
883
+ "build",
884
+ ".output",
885
+ "vendor",
886
+ "__pycache__",
887
+ ".venv",
888
+ "venv",
889
+ ".cartographer",
890
+ ".claude",
891
+ ".ruff_cache",
892
+ ".DS_Store",
893
+ "coverage",
894
+ ".turbo",
895
+ ".vercel",
896
+ ".netlify",
897
+ "Pods",
898
+ "DerivedData",
899
+ ".build",
900
+ ".swiftpm"
901
+ ]);
902
+ //#endregion
903
+ //#region src/policy/cartographer/frontmatter.ts
904
+ /**
905
+ * Frontmatter parsing — pure text helpers (no fs). Ports `parse_frontmatter.py`.
906
+ */
907
+ const BLOCK_SCALARS = /* @__PURE__ */ new Set([
908
+ "|",
909
+ ">",
910
+ "|+",
911
+ "|-",
912
+ ">+",
913
+ ">-"
914
+ ]);
915
+ /** Escape regex metacharacters in an arbitrary field name. */
916
+ function escapeRe(s) {
917
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
918
+ }
919
+ /**
920
+ * Extract a single frontmatter field's value from `text`. Strips surrounding
921
+ * quotes; skips YAML block-scalar markers. Returns "" when absent.
922
+ * @param text - The full document text.
923
+ * @param field - The frontmatter key to read.
924
+ * @returns The field value, or "".
925
+ */
926
+ function parseField(text, field) {
927
+ const fm = /^---\s*\n([\s\S]*?)\n---/.exec(text);
928
+ if (!fm || fm[1] === void 0) return "";
929
+ const lineRe = new RegExp(`^${escapeRe(field)}\\s*:\\s*(.+)$`);
930
+ for (const line of fm[1].split("\n")) {
931
+ const m = lineRe.exec(line);
932
+ if (!m || m[1] === void 0) continue;
933
+ const val = m[1].trim().replace(/^["']|["']$/g, "");
934
+ if (BLOCK_SCALARS.has(val)) continue;
935
+ return val;
936
+ }
937
+ return "";
938
+ }
939
+ /**
940
+ * Derive a short description from the body following the frontmatter: the first
941
+ * non-empty trimmed line, sliced to `maxLen`. Returns "" when none.
942
+ * @param text - The full document text.
943
+ * @param maxLen - Maximum length of the returned description.
944
+ * @returns The body-derived description, or "".
945
+ */
946
+ function parseBodyDesc(text, maxLen = 60) {
947
+ const m = /^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)/.exec(text);
948
+ if (!m || m[1] === void 0) return "";
949
+ for (const raw of m[1].split("\n")) {
950
+ const line = raw.trim();
951
+ if (line) return line.slice(0, maxLen);
952
+ }
953
+ return "";
954
+ }
955
+ //#endregion
956
+ //#region src/policy/cartographer/entry.ts
957
+ const ENTRY_RE = /^(.*?)\[([^\]]+)\]\(([^)]+)\)\s*(?:—|-{1,2})\s*(.*)$/;
958
+ const ENRICH_RE = /^(?:.*?)\[([^\]]+)\]\(([^)]+)\)\s*(?:—|-{1,2})\s*(.+)$/;
959
+ /**
960
+ * Parse a `merge_index` tree line into its parts. Returns null on no match.
961
+ * @param line - The raw tree line.
962
+ * @returns The parsed entry, or null.
963
+ */
964
+ function parseEntry(line) {
965
+ const m = ENTRY_RE.exec(line);
966
+ if (!m || m[1] === void 0 || m[2] === void 0 || m[3] === void 0 || m[4] === void 0) return null;
967
+ return {
968
+ prefix: m[1],
969
+ name: m[2],
970
+ path: m[3],
971
+ desc: m[4]
972
+ };
973
+ }
974
+ /**
975
+ * Parse an enrichment line into `[path, desc]`, requiring a non-empty desc.
976
+ * @param line - The raw index line.
977
+ * @returns The `[path, desc]` pair, or null.
978
+ */
979
+ function parseEnrichment(line) {
980
+ const m = ENRICH_RE.exec(line);
981
+ if (!m || m[2] === void 0 || m[3] === void 0) return null;
982
+ const desc = m[3].trim();
983
+ return desc ? [m[2], desc] : null;
984
+ }
985
+ //#endregion
986
+ //#region src/policy/cartographer/describe.ts
987
+ /**
988
+ * File-description heuristics — pure text in, description out (no fs). Ports
989
+ * `describe.py`.
990
+ */
991
+ const SOURCE_SUFFIXES = /* @__PURE__ */ new Set([
992
+ ".ts",
993
+ ".tsx",
994
+ ".js",
995
+ ".jsx",
996
+ ".py",
997
+ ".swift"
998
+ ]);
999
+ /**
1000
+ * First `# ` Markdown heading text (sans hashes), sliced to 60. "" when none.
1001
+ * @param text - The document text.
1002
+ * @returns The heading text, or "".
1003
+ */
1004
+ function firstHeading(text) {
1005
+ for (const line of text.split("\n")) if (line.startsWith("# ")) return line.replace(/^#+/, "").trim().slice(0, 60);
1006
+ return "";
1007
+ }
1008
+ /**
1009
+ * First leading comment among the first 10 lines (`//`, `#` but not `#!`, or a
1010
+ * `"""`/`'''` docstring), sliced to 60. "" when none.
1011
+ * @param text - The source text.
1012
+ * @returns The comment text, or "".
1013
+ */
1014
+ function firstComment(text) {
1015
+ const lines = text.split("\n").slice(0, 10);
1016
+ for (const raw of lines) {
1017
+ const line = raw.trim();
1018
+ if ((line.startsWith("//") || line.startsWith("#")) && !line.startsWith("#!")) return line.replace(/^[/#! ]+/, "").slice(0, 60);
1019
+ if (line.startsWith("\"\"\"") || line.startsWith("'''")) return line.replace(/^['"\s]+|['"\s]+$/g, "").slice(0, 60);
1020
+ }
1021
+ return "";
1022
+ }
1023
+ /**
1024
+ * Derive a description from a file's suffix + text. For `.md`, the supplied
1025
+ * frontmatter `description` (truncated) wins over the first heading; for known
1026
+ * source suffixes, the first comment; else "".
1027
+ * @param suffix - The file extension (with dot).
1028
+ * @param text - The file text.
1029
+ * @param mdField - The pre-parsed frontmatter `description` (md only).
1030
+ * @returns The derived description, or "".
1031
+ */
1032
+ function descFromText(suffix, text, mdField) {
1033
+ if (suffix === ".md") return mdField.slice(0, 60) || firstHeading(text);
1034
+ if (SOURCE_SUFFIXES.has(suffix)) return firstComment(text);
1035
+ return "";
1036
+ }
1037
+ //#endregion
1038
+ export { solidReadGate as A, capVerbosity as C, docConsultedGate as D, brainstormGate as E, requiredArchSkill as F, detectModularArchitecture as M, detectProjectType as N, evaluateApex as O, isApexCommand as P, MAX_TOKENS as S, APEX_GATES as T, detectRequiredSkills as _, parseEntry as a, frameworkSolidGate as b, EXCLUDE_DIRS as c, buildApexTaskInjection as d, loadApexTaskState as f, detectClaudeMdProjectType as g, buildClaudeMdContext as h, parseEnrichment as i, DEV_KEYWORDS as j, freshnessGate as k, PROJECT_INDICATORS as l, buildApexInstruction as m, firstComment as n, parseBodyDesc as o, DEV_VERBS as p, firstHeading as r, parseField as s, descFromText as t, buildApexTaskContext as u, skillTriggerGate as v, detectCreationIntent as w, MAX_EXA_RESULTS as x, SKILL_TRIGGERS as y };