@genesislcap/genx 15.35.0 → 15.35.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/README.md CHANGED
@@ -46,11 +46,17 @@ files, the command looks up the tree as far as the repo root and, if it finds th
46
46
  at it and writes nothing rather than leaving an orphaned second copy behind (`--tools` installs in
47
47
  the current folder anyway).
48
48
 
49
- The rule markdown is copied to `docs/agent-rules/` and referenced from a managed block in
50
- `CLAUDE.md`, `GEMINI.md`, or `AGENTS.md`, and from a generated `.cursor/rules/<rule>.mdc` for
51
- Cursor. Content outside the managed block is never touched. Installed copies are managed — re-run
52
- the command after `genx upgrade` to pick up rule changes, and propose edits in
53
- [foundation-ui](https://github.com/genesislcap/foundation-ui) rather than locally.
49
+ The rule markdown is copied to `docs/agent-rules/`, and each assistant is wired in its own idiom:
50
+ a `.claude/skills/<rule>/SKILL.md` for Claude Code, a `.cursor/rules/<rule>.mdc` for Cursor, and a
51
+ pointer line inside a managed block in `GEMINI.md` or `AGENTS.md` for the rest. All four load the
52
+ rule body on demand rather than on every session. Content outside the managed block is never
53
+ touched.
54
+
55
+ Installed copies are managed: re-run the command after `genx upgrade` **and `npm install`** to pick
56
+ up rule changes, and propose edits in
57
+ [foundation-ui](https://github.com/genesislcap/foundation-ui) rather than locally. The install
58
+ records what it did in `docs/agent-rules/.genx-agent-rules.json`, which is what lets `--check`
59
+ verify the assistants actually chosen and lets a later run sweep away rules retired upstream.
54
60
 
55
61
  Currently shipped:
56
62
 
@@ -15,10 +15,14 @@
15
15
 
16
16
  ## When this rule applies
17
17
 
18
- Apply it before writing code, as soon as any of these is true:
18
+ Most of what a consumer project builds is app code, and app code is not this rule's business.
19
+ The signal is not "I am writing a component" — it is "I am writing something a *different* Genesis
20
+ project would want unchanged". Apply the rule before writing code when that is plausible and any of
21
+ these is true:
19
22
 
20
23
  - You are about to build a generic capability — a UI component, a formatter, a datasource or grid
21
- adapter, an auth/comms/layout concern — with no business rules in it.
24
+ adapter, an auth/comms/layout concern — with no business rules in it, and Step 2's test looks
25
+ likely to pass.
22
26
  - You are extending an existing `@genesislcap/*` component by wrapping, subclassing, or
23
27
  re-implementing it, because it lacks one attribute, slot, event, or option.
24
28
  - Someone says "we built this in the other app" or you can see code copied in from another Genesis
@@ -53,27 +57,40 @@ It does **not** apply to app code, and most code is app code:
53
57
 
54
58
  Platform surface is large and under-documented; assume it might already be there.
55
59
 
60
+ First find `node_modules`: in a Genesis app it is under `client/`, while these rules are installed
61
+ at the repo root, so a relative path from your cwd will usually miss. Resolve it once, and do not
62
+ silence the errors — "not installed" and "searched, found nothing" must not look alike:
63
+
56
64
  ```bash
57
- # Search the installed packages' public types for the capability.
58
- grep -rl "<CapabilityName>" node_modules/@genesislcap/*/dist/dts 2>/dev/null | head
65
+ MODULES=$(cd client 2>/dev/null || cd .; npm root) # absolute path to node_modules
66
+ ls "$MODULES/@genesislcap" | head
67
+ ```
59
68
 
60
- # What does the package actually export?
61
- cat node_modules/@genesislcap/<pkg>/dist/dts/index.d.ts | grep -i "<keyword>"
69
+ Then search for the **behaviour**, not for the name you are about to invent, and search the leaf
70
+ declarations — `index.d.ts` is a 20-line barrel of `export *`, so grepping it answers nothing, and
71
+ `docs/api/` is not published (the tarball ships an empty `docs/`; `README.md` and `dist/dts` do):
62
72
 
63
- # Newer versions may already have it.
64
- npm view @genesislcap/<pkg> version
65
- npm view @genesislcap/<pkg> dist-tags
73
+ ```bash
74
+ grep -rin "<behaviour keyword>" "$MODULES/@genesislcap/<pkg>/dist/dts" | head
75
+ grep -rin "<behaviour keyword>" "$MODULES/@genesislcap/<pkg>/README.md" | head
76
+
77
+ # Newer versions may already have it. Read the release notes, not the dist-tag list.
78
+ npm view @genesislcap/<pkg> dist-tags.latest
66
79
  ```
67
80
 
68
- Also check the package's `README.md` and `docs/api/` inside `node_modules`, and the showcase apps
69
- if you have the monorepo to hand. If it exists, use it and stop here.
81
+ Try two or three phrasings of the behaviour before concluding it is absent, and check the showcase
82
+ apps if you have the monorepo to hand. If it exists, use it and stop here.
70
83
 
71
84
  ## Step 2 — Apply the generality test
72
85
 
73
86
  Answer all four. Any "no" means it is app code: build it locally and stop.
74
87
 
75
88
  1. **Name two independent consumers.** Two real projects or teams that would use this, not two
76
- screens in this app.
89
+ screens in this app. You are inside one checkout and cannot see the others, so the honest
90
+ answers are: name them because the human or the repo told you, **ask the human to name them**,
91
+ or write "demand unestablished — one project so far" in the proposal. Never invent a team or an
92
+ app name; a fabricated `## Who needs it` is worse than an empty one, because it is the field the
93
+ platform team prioritises on.
77
94
  2. **Describe the API without this app's domain nouns.** If the signature needs
78
95
  `Trade`, `Counterparty`, or your client's name to make sense, it is not platform capability.
79
96
  3. **No app-specific dependencies.** It must not reach into this app's services, routes, config
@@ -119,7 +136,8 @@ Title: what the capability is, in the platform's vocabulary — not your app's.
119
136
  - Good: `grid-pro: expose a column-level formatter hook for cell values`
120
137
  - Bad: `Need to format trade dates in the blotter`
121
138
 
122
- Body:
139
+ Body — the repo ships an issue form at `.github/ISSUE_TEMPLATE/upstream-proposal.yml` whose fields
140
+ are the headings below; fill the same ones when filing from the CLI:
123
141
 
124
142
  ````markdown
125
143
  ## What's missing
@@ -175,6 +193,12 @@ areas as the bug rule (`Grids`, `COMMS`, `Design System`, `Forms`, `Layout`,
175
193
  `Frontends Compatibility`, …). Add `Next` when it is capability the platform is expected to grow
176
194
  into rather than something needed this sprint. Delete the temp file, then report the URL.
177
195
 
196
+ > **If the tracker is unreachable** — `genesislcap/foundation-ui` is private and many consumer
197
+ > developers will not have access (`gh repo view genesislcap/foundation-ui --json name` tells you in
198
+ > one call) — or the human declines, say so plainly, record the proposal in
199
+ > `docs/upstream-tracking.md` as **unfiled**, and carry on to Step 6 with
200
+ > `UPSTREAM-CANDIDATE(unfiled)`. Nothing here should block the app.
201
+
178
202
  ## Step 6 — Build locally as a seam, with approval
179
203
 
180
204
  A filed proposal does not ship your feature. Once the issue URL exists:
@@ -205,10 +229,11 @@ A filed proposal does not ship your feature. Once the issue URL exists:
205
229
  ## Step 7 — Offer the PR
206
230
 
207
231
  Proposals with a PR attached land far sooner than proposals without one, and you have already
208
- written the implementation once. `foundation-ui` accepts contributions from consumer teams — see its
209
- [CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md) for
210
- setup, commit format, and the PR template. A pre-release can be cut from the branch so this app can
211
- adopt it before it merges (`@genesislcap/<pkg>@<dist-tag>`); ask in the issue.
232
+ written the implementation once. Ask in the issue for the two things a consumer team cannot provide
233
+ itself: a Jira ticket reference for the commit message (commit linting enforces one) and, if you
234
+ need to adopt the change before it merges, a pre-release — cutting one is an internal Actions
235
+ dispatch a platform engineer runs. Setup and PR conventions are in
236
+ [CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md).
212
237
 
213
238
  ## Never
214
239
 
@@ -13,9 +13,14 @@
13
13
 
14
14
  ## When this rule applies
15
15
 
16
- Apply it as soon as any of these is true:
16
+ First, a caution about the trigger: in an app built on the platform, `@genesislcap` frames appear
17
+ in nearly every stack — FAST's template and observable machinery, the router, DI and comms all call
18
+ back into app code. A platform frame is *not* on its own a sign of a platform bug. What matters is
19
+ the **innermost non-platform frame**: if that is your own code, this is your bug.
17
20
 
18
- - A stack frame, breakpoint, or console error points into `node_modules/@genesislcap/**`.
21
+ Apply the rule when that test points upstream and any of these is true:
22
+
23
+ - The innermost non-platform frame is inside `node_modules/@genesislcap/**`.
19
24
  - The behaviour of a `@genesislcap/*` package contradicts its own docs, types, JSDoc, or
20
25
  README — including a `CustomEvent<unknown>` where a typed detail is documented.
21
26
  - You are about to write, or have caught yourself considering, any of these:
@@ -43,6 +48,30 @@ Step 1 exists to tell those apart.
43
48
  5. **Be honest about what you verified.** Separate "confirmed by running it" from "suspected".
44
49
  A confidently-wrong root cause wastes more upstream time than no root cause at all.
45
50
 
51
+ ## Before you start: can you reach the tracker?
52
+
53
+ `genesislcap/foundation-ui` is private. Filing needs GitHub CLI authenticated as someone with
54
+ access to it (`gh auth status`), which many consumer-project developers will not have — the failure
55
+ looks like `Could not resolve to a Repository with the name 'genesislcap/foundation-ui'`.
56
+
57
+ Check once, before promising the human an issue:
58
+
59
+ ```bash
60
+ gh repo view genesislcap/foundation-ui --json name >/dev/null && echo reachable
61
+ ```
62
+
63
+ **If it is not reachable, or the human declines to file, or they simply do not answer**, the rule
64
+ does not leave the app stuck. Non-negotiable 1 is lifted, and instead:
65
+
66
+ 1. Say plainly that the upstream issue could not be filed, and why.
67
+ 2. Write the report you would have filed into `docs/upstream-tracking.md` (or the project's own
68
+ notes) so nothing is lost, marked as **unfiled**.
69
+ 3. Proceed to Step 6's workaround flow — still with explicit approval, still marked — using
70
+ `UPSTREAM-WORKAROUND(unfiled)` in place of the issue reference.
71
+ 4. Tell the human who to ask for repo access, or to have someone with access file it.
72
+
73
+ A production outage does not wait for a tracker.
74
+
46
75
  ---
47
76
 
48
77
  ## Step 1 — Rule out the boring causes
@@ -53,15 +82,20 @@ Do this before writing a single line of the issue.
53
82
  # What is actually installed (the lockfile, not the range in package.json)?
54
83
  npm ls @genesislcap/<package> # or: pnpm why @genesislcap/<package>
55
84
 
56
- # Is it the latest? Is there a newer patch or a prerelease dist-tag?
57
- npm view @genesislcap/<package> version
58
- npm view @genesislcap/<package> dist-tags
85
+ # Is it the latest?
86
+ npm view @genesislcap/<package> dist-tags.latest
87
+ npm view @genesislcap/<package> versions --json | tail -20
59
88
 
60
89
  # Are the @genesislcap/* packages on matching versions? Mixed majors/minors cause
61
90
  # symptoms that look exactly like upstream bugs but are dependency drift.
62
91
  npm ls --all 2>/dev/null | grep @genesislcap
63
92
  ```
64
93
 
94
+ Do not scan the full dist-tag list for something newer: the packages carry ~200 per-ticket
95
+ pre-release tags, most of them on older majors (`bny: 14.49.0-bny.2`, `pa-659: 13.2.0-pa-659.2`),
96
+ they are not ordered, and their names imply nothing about content. "Upgrading" to one is usually a
97
+ downgrade. Check release notes instead.
98
+
65
99
  Then confirm the calling code follows the documented API — read the package's own `dist/dts/*.d.ts`
66
100
  and README rather than assuming. If the answer is "we were holding it wrong", fix it locally and
67
101
  stop here; this rule is done.
@@ -111,8 +145,10 @@ conventional-commit prefix.
111
145
  - Good: `sendForStream() defers DATA_LOGON via DOM.queueUpdate — StrictMode double-mount discards it, leaving every dataserver grid empty`
112
146
  - Bad: `Grid is empty`
113
147
 
114
- Body — use this structure (it mirrors
115
- [#2442](https://github.com/genesislcap/foundation-ui/issues/2442), a good reference report):
148
+ Body — the repo ships an issue form at `.github/ISSUE_TEMPLATE/upstream-bug.yml`, and its fields
149
+ are the structure below. Fill the same headings when filing from the CLI, so a CLI-filed issue and
150
+ a browser-filed one read alike ([#2442](https://github.com/genesislcap/foundation-ui/issues/2442)
151
+ is a good worked example):
116
152
 
117
153
  ````markdown
118
154
  ## Package + version
@@ -237,11 +273,18 @@ Filing the issue does not unblock this app. Once the issue URL exists:
237
273
 
238
274
  ## Step 7 — Offer the upstream fix
239
275
 
240
- If the fix is small and you can see it, say so in the issue and offer a PR. `foundation-ui` accepts
241
- contributions from consumer teams — see its
242
- [CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md) for
243
- setup, commit format, and the PR template. A pre-release build can be cut from a branch so this app
244
- can validate the fix before it lands (`@genesislcap/<pkg>@<dist-tag>`); ask in the issue.
276
+ If the fix is small and you can see it, say so in the issue and offer a PR — but ask for two things
277
+ in the issue rather than trying and failing:
278
+
279
+ - **A ticket reference.** Every commit needs an `FUI-`/`PLAT-` style Jira reference and it is
280
+ enforced by commit linting, which consumer teams cannot mint for themselves. Ask a maintainer to
281
+ supply one on the issue.
282
+ - **A pre-release, if you need to validate before the fix merges.** Cutting one is an internal
283
+ Actions dispatch a platform engineer runs; it is not something a consumer team can trigger. Ask
284
+ for it in the issue and say why you need it, rather than assuming it is on tap.
285
+
286
+ Setup and PR conventions are in
287
+ [CONTRIBUTING.md](https://github.com/genesislcap/foundation-ui/blob/master/CONTRIBUTING.md).
245
288
 
246
289
  ## Never
247
290
 
@@ -1 +1 @@
1
- {"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/commands/agent-rules.ts"],"names":[],"mappings":"yBAyCE,eAAsB,EACtB,UAAS;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO;AAFnD,wBAkEE"}
1
+ {"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/commands/agent-rules.ts"],"names":[],"mappings":"yBAkEE,eAAsB,EACtB,UAAS;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO;AAFnD,wBAmFE"}
@@ -8,31 +8,56 @@ const consola_1 = tslib_1.__importDefault(require("consola"));
8
8
  const agent_rules_1 = require("../utils/agent-rules");
9
9
  /** The packaged rules live at the root of this package, alongside `bin` and `dist`. */
10
10
  const packagedRulesDir = () => (0, node_path_1.resolve)(__dirname, '../../agent-rules');
11
- const loadManifest = (rulesDir) => JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(rulesDir, 'manifest.json'), 'utf8'));
12
- const readRuleContents = (rulesDir, manifest) => manifest.rules.reduce((contents, rule) => {
13
- contents[rule.file] = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rulesDir, rule.file), 'utf8');
14
- return contents;
15
- }, {});
16
- const resolveTools = (dir, requested) => {
17
- if (!requested) {
18
- return (0, agent_rules_1.detectTools)(dir);
11
+ /** A packaging fault, not a user error — say which file is missing instead of dumping a stack. */
12
+ const fail = (message) => {
13
+ consola_1.default.error(message);
14
+ return process.exit(1);
15
+ };
16
+ const loadManifest = (rulesDir) => {
17
+ const path = (0, node_path_1.join)(rulesDir, 'manifest.json');
18
+ try {
19
+ return JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
19
20
  }
21
+ catch (error) {
22
+ return fail(`Could not read the packaged rules manifest at ${path}: ${(0, agent_rules_1.errorMessage)(error)}`);
23
+ }
24
+ };
25
+ const readRuleContents = (rulesDir, manifest) => manifest.rules.reduce((contents, rule) => {
20
26
  try {
21
- return (0, agent_rules_1.parseTools)(requested);
27
+ contents[rule.file] = (0, node_fs_1.readFileSync)((0, node_path_1.join)(rulesDir, rule.file), 'utf8');
22
28
  }
23
29
  catch (error) {
24
- // A typo in --tools is a user error, not a bug: report it without a stack trace.
25
- consola_1.default.error((0, agent_rules_1.errorMessage)(error));
26
- process.exit(1);
30
+ fail(`The manifest lists "${rule.file}" but the packaged rule is missing: ${(0, agent_rules_1.errorMessage)(error)}`);
27
31
  }
32
+ return contents;
33
+ }, {});
34
+ /**
35
+ * What was installed beats what happens to be lying around. Re-detecting on every run would tell a
36
+ * team that installed `--tools claude` that their deliberate omissions are drift, and would let a
37
+ * hand-edited file go unchecked when CI passes a narrower `--tools` than the install did.
38
+ */
39
+ const resolveTools = (dir, targetDir, requested) => {
40
+ var _a;
41
+ if (requested !== undefined) {
42
+ try {
43
+ return (0, agent_rules_1.parseTools)(requested);
44
+ }
45
+ catch (error) {
46
+ // A typo in --tools is a user error, not a bug: report it without a stack trace.
47
+ return fail((0, agent_rules_1.errorMessage)(error));
48
+ }
49
+ }
50
+ const installed = (0, agent_rules_1.readInstalledState)(dir, targetDir);
51
+ return ((_a = installed === null || installed === void 0 ? void 0 : installed.tools) === null || _a === void 0 ? void 0 : _a.length) ? installed.tools : (0, agent_rules_1.detectTools)(dir);
28
52
  };
29
53
  exports.default = (...args_1) => tslib_1.__awaiter(void 0, [...args_1], void 0, function* (folder = process.cwd(), options = {}) {
30
54
  const dir = (0, node_path_1.resolve)(folder);
31
55
  const rulesDir = packagedRulesDir();
32
56
  const manifest = loadManifest(rulesDir);
33
57
  const ruleContents = readRuleContents(rulesDir, manifest);
34
- const tools = resolveTools(dir, options.tools);
35
- if (!tools.length && !options.tools) {
58
+ const tools = resolveTools(dir, manifest.targetDir, options.tools);
59
+ const installed = (0, agent_rules_1.readInstalledState)(dir, manifest.targetDir);
60
+ if (!tools.length && options.tools === undefined) {
36
61
  // Writing rule files into a package directory whose assistant files live higher up leaves an
37
62
  // orphaned second copy behind, so point at the real root instead of quietly installing here.
38
63
  const assistantRoot = (0, agent_rules_1.findAssistantRoot)(dir);
@@ -54,17 +79,21 @@ exports.default = (...args_1) => tslib_1.__awaiter(void 0, [...args_1], void 0,
54
79
  process.exit(1);
55
80
  }
56
81
  const drift = (0, agent_rules_1.findDrift)(dir, planned);
82
+ // Rules retired upstream: their files linger, and a stale .mdc keeps pointing agents at a rule
83
+ // nobody maintains any more.
84
+ const stale = (0, agent_rules_1.findStale)(installed, planned).filter((path) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, path)));
57
85
  if (options.check) {
58
- if (drift.length) {
86
+ if (drift.length || stale.length) {
59
87
  consola_1.default.error(`Agent rules are out of date in ${dir}:`);
60
88
  drift.forEach((file) => consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(file.path)} ${(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, file.path)) ? 'differs' : 'missing'}`)));
89
+ stale.forEach((path) => consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(path)} no longer shipped`)));
61
90
  consola_1.default.info('Run `genx agent-rules` to update.');
62
91
  process.exit(1);
63
92
  }
64
93
  consola_1.default.success(`Agent rules are up to date (${manifest.rules.length} rule(s)).`);
65
94
  return;
66
95
  }
67
- if (!drift.length) {
96
+ if (!drift.length && !stale.length) {
68
97
  consola_1.default.info('Agent rules already up to date. Nothing to do.');
69
98
  return;
70
99
  }
@@ -74,6 +103,18 @@ exports.default = (...args_1) => tslib_1.__awaiter(void 0, [...args_1], void 0,
74
103
  (0, node_fs_1.writeFileSync)(path, file.contents, 'utf8');
75
104
  consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(file.path)}`));
76
105
  });
106
+ stale.forEach((relative) => {
107
+ const path = (0, node_path_1.join)(dir, relative);
108
+ (0, node_fs_1.unlinkSync)(path);
109
+ try {
110
+ // Skill folders are one-file directories; leaving the husk behind is just litter.
111
+ (0, node_fs_1.rmdirSync)((0, node_path_1.dirname)(path));
112
+ }
113
+ catch (_a) {
114
+ // Not empty, or shared with something else. Fine either way.
115
+ }
116
+ consola_1.default.log((0, build_kit_1.white)(` ${(0, build_kit_1.bold)(relative)} removed`));
117
+ });
77
118
  consola_1.default.success(`Installed ${manifest.rules.length} agent rule(s)${tools.length ? ` for: ${tools.join(', ')}` : ''}.`);
78
119
  consola_1.default.info('Commit the changes so the whole team gets them.');
79
120
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ;;aAY2qB,CAAC;aAAe,CAAC;;;;oBAVlsB,CAAA;;;;;;;CAOd,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,QAAQ;;aAYspD,CAAC;aAAe,CAAC;;;;oBAV7qD,CAAA;;;;;;;CAOd,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,QAAQ,CAAC"}
@@ -24,7 +24,21 @@ export interface AgentRulesManifest {
24
24
  export declare const AGENT_TOOLS: readonly ["claude", "cursor", "gemini", "agents"];
25
25
  export type AgentTool = (typeof AGENT_TOOLS)[number];
26
26
  /** Entry file each assistant reads, for the tools wired via a managed block. */
27
- export declare const TOOL_ENTRY_FILES: Record<Exclude<AgentTool, 'cursor'>, string>;
27
+ export declare const TOOL_ENTRY_FILES: Record<Extract<AgentTool, 'gemini' | 'agents'>, string>;
28
+ /** Where the installed state lives, relative to `targetDir`. */
29
+ export declare const STATE_FILE = ".genx-agent-rules.json";
30
+ /**
31
+ * What the last install actually did. `--check` reads this instead of re-detecting, so a team that
32
+ * installed `--tools claude` is not told its deliberate omissions are drift; and the file list is
33
+ * what lets a later run sweep away rules that have since been retired upstream.
34
+ */
35
+ export interface InstalledState {
36
+ /** `manifest.version` at install time — the hook for changing the sweep rules later. */
37
+ version: number;
38
+ tools: AgentTool[];
39
+ /** Every path this command owns, relative to the project root. */
40
+ files: string[];
41
+ }
28
42
  export declare const MANAGED_BEGIN = "<!-- BEGIN genx agent-rules -->";
29
43
  export declare const MANAGED_END = "<!-- END genx agent-rules -->";
30
44
  export interface PlannedFile {
@@ -34,6 +48,13 @@ export interface PlannedFile {
34
48
  }
35
49
  export type ExistsFn = (path: string) => boolean;
36
50
  export type ReadFn = (path: string) => string;
51
+ /**
52
+ * Line endings, ignored. A Windows checkout of the consumer repo hands us CRLF (Git for Windows
53
+ * defaults to `core.autocrlf=true`, and `* text=auto` does it for everyone) while the packaged
54
+ * rules stay LF, because npm tarballs are not EOL-converted. Comparing raw text would fail
55
+ * `--check` on every CI run, and rewriting would not fix it — the next checkout re-normalises.
56
+ */
57
+ export declare const normalizeEol: (value: string) => string;
37
58
  /**
38
59
  * Narrowed message for a caught value. `catch` bindings are only typed loosely because this
39
60
  * package has not enabled `useUnknownInCatchVariables` yet; don't rely on that staying true.
@@ -50,9 +71,14 @@ export declare const errorMessage: (error: unknown) => string;
50
71
  * option that cannot lose someone's instructions.
51
72
  */
52
73
  export declare const upsertManagedBlock: (content: string, body: string) => string;
53
- /** Import line understood by Claude Code (`CLAUDE.md`) and Gemini CLI (`GEMINI.md`). */
54
- export declare const renderImportBlock: (rules: AgentRule[], targetDir: string) => string;
55
- /** Plain pointer for assistants without an import syntax (AGENTS.md, Copilot, Windsurf, …). */
74
+ /**
75
+ * Progressive-disclosure wiring for Claude Code: a project skill whose front matter carries the
76
+ * trigger, so the rule body is read when it is relevant instead of on every session start. This is
77
+ * the analogue of Cursor's `alwaysApply: false` — an `@import` in `CLAUDE.md` would load ~5k tokens
78
+ * of rule text into every session in the project, relevant or not.
79
+ */
80
+ export declare const renderClaudeSkill: (rule: AgentRule, targetDir: string) => string;
81
+ /** Plain pointer for assistants without a skill or rules mechanism (GEMINI.md, AGENTS.md, …). */
56
82
  export declare const renderPointerBlock: (rules: AgentRule[], targetDir: string) => string;
57
83
  /** A fully generated Cursor rule that defers to the shared markdown. */
58
84
  export declare const renderCursorRule: (rule: AgentRule, targetDir: string) => string;
@@ -70,13 +96,26 @@ export declare const detectTools: (dir: string, exists?: ExistsFn) => AgentTool[
70
96
  * directory the assistants actually read.
71
97
  */
72
98
  export declare const findAssistantRoot: (dir: string, exists?: ExistsFn) => string | null;
73
- /** Parses and validates a `--tools` value. Throws on anything unknown rather than guessing. */
74
- export declare const parseTools: (value: string) => AgentTool[];
99
+ /**
100
+ * Parses and validates a `--tools` value. Throws on anything unknown, and on anything that names
101
+ * no tool at all — the CLI parser hands us whatever the shell gave it (`--tools ""` arrives as the
102
+ * number `0`, repeated `-t` flags as an array), and an explicit flag that wires nothing is far
103
+ * more likely a typo than an intent.
104
+ */
105
+ export declare const parseTools: (value: unknown) => AgentTool[];
75
106
  /**
76
107
  * Everything the command intends to write, as (path, contents) pairs. Kept separate from the
77
108
  * writing itself so `--check` can compare against disk without touching it.
78
109
  */
79
110
  export declare const planFiles: (dir: string, manifest: AgentRulesManifest, tools: AgentTool[], ruleContents: Record<string, string>, read: ReadFn, exists?: ExistsFn) => PlannedFile[];
111
+ /** The state recorded by the last install, or `null` when there is none to read. */
112
+ export declare const readInstalledState: (dir: string, targetDir: string, read?: ReadFn, exists?: ExistsFn) => InstalledState | null;
113
+ /**
114
+ * Files a previous install owned that the current manifest no longer produces — a rule renamed or
115
+ * retired upstream. Left in place, a stale `.mdc` keeps pointing Cursor at a withdrawn rule and
116
+ * agents keep following it, with `--check` none the wiser.
117
+ */
118
+ export declare const findStale: (state: InstalledState | null, planned: PlannedFile[]) => string[];
80
119
  /** Planned files whose contents differ from what is on disk. */
81
120
  export declare const findDrift: (dir: string, planned: PlannedFile[], read?: ReadFn, exists?: ExistsFn) => PlannedFile[];
82
121
  //# sourceMappingURL=agent-rules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/utils/agent-rules.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,mDAAoD,CAAC;AAC7E,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAIzE,CAAC;AAEF,eAAO,MAAM,aAAa,oCAAoC,CAAC;AAC/D,eAAO,MAAM,WAAW,kCAAkC,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;AACjD,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAK9C;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MACU,CAAC;AAEzD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,EAAE,MAAM,MAAM,KAAG,MAsBlE,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,iBAAiB,GAAI,OAAO,SAAS,EAAE,EAAE,WAAW,MAAM,KAAG,MAI5D,CAAC;AAEf,+FAA+F;AAC/F,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,EAAE,EAAE,WAAW,MAAM,KAAG,MAI7D,CAAC;AAUf,wEAAwE;AACxE,eAAO,MAAM,gBAAgB,GAAI,MAAM,SAAS,EAAE,WAAW,MAAM,KAAG,MAarE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,EAAE,SAAQ,QAAqB,KAAG,SAAS,EAQjF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,EAAE,SAAQ,QAAqB,KAAG,MAAM,GAAG,IAiBvF,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,SAAS,EAYnD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,EACX,UAAU,kBAAkB,EAC5B,OAAO,SAAS,EAAE,EAClB,cAAc,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,MAAM,MAAM,EACZ,SAAQ,QAAqB,KAC5B,WAAW,EAiCb,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,SAAS,GACpB,KAAK,MAAM,EACX,SAAS,WAAW,EAAE,EACtB,OAAM,MAA6C,EACnD,SAAQ,QAAqB,KAC5B,WAAW,EAIV,CAAC"}
1
+ {"version":3,"file":"agent-rules.d.ts","sourceRoot":"","sources":["../../src/utils/agent-rules.ts"],"names":[],"mappings":"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"}
@@ -1,18 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.findDrift = exports.planFiles = exports.parseTools = exports.findAssistantRoot = exports.detectTools = exports.renderCursorRule = exports.renderPointerBlock = exports.renderImportBlock = exports.upsertManagedBlock = exports.errorMessage = exports.MANAGED_END = exports.MANAGED_BEGIN = exports.TOOL_ENTRY_FILES = exports.AGENT_TOOLS = void 0;
3
+ exports.findDrift = exports.findStale = exports.readInstalledState = exports.planFiles = exports.parseTools = exports.findAssistantRoot = exports.detectTools = exports.renderCursorRule = exports.renderPointerBlock = exports.renderClaudeSkill = exports.upsertManagedBlock = exports.errorMessage = exports.normalizeEol = exports.MANAGED_END = exports.MANAGED_BEGIN = exports.STATE_FILE = exports.TOOL_ENTRY_FILES = exports.AGENT_TOOLS = void 0;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  exports.AGENT_TOOLS = ['claude', 'cursor', 'gemini', 'agents'];
7
7
  /** Entry file each assistant reads, for the tools wired via a managed block. */
8
8
  exports.TOOL_ENTRY_FILES = {
9
- claude: 'CLAUDE.md',
10
9
  gemini: 'GEMINI.md',
11
10
  agents: 'AGENTS.md',
12
11
  };
12
+ /** Where the installed state lives, relative to `targetDir`. */
13
+ exports.STATE_FILE = '.genx-agent-rules.json';
13
14
  exports.MANAGED_BEGIN = '<!-- BEGIN genx agent-rules -->';
14
15
  exports.MANAGED_END = '<!-- END genx agent-rules -->';
15
16
  const countOccurrences = (haystack, needle) => haystack.split(needle).length - 1;
17
+ /**
18
+ * Line endings, ignored. A Windows checkout of the consumer repo hands us CRLF (Git for Windows
19
+ * defaults to `core.autocrlf=true`, and `* text=auto` does it for everyone) while the packaged
20
+ * rules stay LF, because npm tarballs are not EOL-converted. Comparing raw text would fail
21
+ * `--check` on every CI run, and rewriting would not fix it — the next checkout re-normalises.
22
+ */
23
+ const normalizeEol = (value) => value.replace(/\r\n/g, '\n');
24
+ exports.normalizeEol = normalizeEol;
25
+ const usesCrlf = (value) => value.includes('\r\n');
26
+ const toCrlf = (value) => (0, exports.normalizeEol)(value).replace(/\n/g, '\r\n');
16
27
  /**
17
28
  * Narrowed message for a caught value. `catch` bindings are only typed loosely because this
18
29
  * package has not enabled `useUnknownInCatchVariables` yet; don't rely on that staying true.
@@ -35,37 +46,51 @@ const upsertManagedBlock = (content, body) => {
35
46
  const ends = countOccurrences(content, exports.MANAGED_END);
36
47
  const begin = content.indexOf(exports.MANAGED_BEGIN);
37
48
  const end = content.indexOf(exports.MANAGED_END);
49
+ // Keep the file's own line endings, or a CRLF file drifts again on the next checkout.
50
+ const write = (value) => (usesCrlf(content) ? toCrlf(value) : value);
38
51
  if (begins === 1 && ends === 1 && end > begin) {
39
52
  const before = content.slice(0, begin);
40
53
  const after = content.slice(end + exports.MANAGED_END.length);
41
- return `${before}${block}${after}`;
54
+ return write(`${before}${block}${after}`);
42
55
  }
43
56
  if (begins === 0 && ends === 0) {
44
- return content.trim() ? `${content.replace(/\s*$/, '')}\n\n${block}\n` : `${block}\n`;
57
+ return write(content.trim() ? `${content.replace(/\s*$/, '')}\n\n${block}\n` : `${block}\n`);
45
58
  }
46
59
  throw new Error(`the genx agent-rules block is malformed: found ${begins} "${exports.MANAGED_BEGIN}" and ${ends} ` +
47
60
  `"${exports.MANAGED_END}" marker(s), expected one matching pair or none. Restore the pair or delete ` +
48
61
  `the block by hand, then run again — refusing to guess, so nothing you wrote is lost.`);
49
62
  };
50
63
  exports.upsertManagedBlock = upsertManagedBlock;
51
- /** Import line understood by Claude Code (`CLAUDE.md`) and Gemini CLI (`GEMINI.md`). */
52
- const renderImportBlock = (rules, targetDir) => [
53
- '<!-- Installed by `genx agent-rules`. Re-run it to update; edits inside this block are lost. -->',
54
- ...rules.map((rule) => `@${targetDir}/${rule.file}`),
55
- ].join('\n');
56
- exports.renderImportBlock = renderImportBlock;
57
- /** Plain pointer for assistants without an import syntax (AGENTS.md, Copilot, Windsurf, …). */
58
- const renderPointerBlock = (rules, targetDir) => [
59
- '<!-- Installed by `genx agent-rules`. Re-run it to update; edits inside this block are lost. -->',
60
- ...rules.map((rule) => `- ${rule.description} Read and follow \`${targetDir}/${rule.file}\`.`),
61
- ].join('\n');
62
- exports.renderPointerBlock = renderPointerBlock;
63
64
  /**
64
65
  * YAML double-quoted scalar for a rule description. A colon, a quote, or a leading `-` in the
65
66
  * manifest would otherwise produce front matter Cursor cannot parse — and the rule would then be
66
67
  * silently inert rather than visibly broken.
67
68
  */
68
69
  const yamlString = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
70
+ /**
71
+ * Progressive-disclosure wiring for Claude Code: a project skill whose front matter carries the
72
+ * trigger, so the rule body is read when it is relevant instead of on every session start. This is
73
+ * the analogue of Cursor's `alwaysApply: false` — an `@import` in `CLAUDE.md` would load ~5k tokens
74
+ * of rule text into every session in the project, relevant or not.
75
+ */
76
+ const renderClaudeSkill = (rule, targetDir) => `---
77
+ name: ${rule.name}
78
+ description: ${yamlString(rule.description)}
79
+ ---
80
+
81
+ <!-- Installed by \`genx agent-rules\`. Re-run it to update; local edits are lost. -->
82
+
83
+ # ${rule.title}
84
+
85
+ Read \`${targetDir}/${rule.file}\` and follow it for the rest of this task.
86
+ `;
87
+ exports.renderClaudeSkill = renderClaudeSkill;
88
+ /** Plain pointer for assistants without a skill or rules mechanism (GEMINI.md, AGENTS.md, …). */
89
+ const renderPointerBlock = (rules, targetDir) => [
90
+ '<!-- Installed by `genx agent-rules`. Re-run it to update; edits inside this block are lost. -->',
91
+ ...rules.map((rule) => `- ${rule.description} Read and follow \`${targetDir}/${rule.file}\`.`),
92
+ ].join('\n');
93
+ exports.renderPointerBlock = renderPointerBlock;
69
94
  /** A fully generated Cursor rule that defers to the shared markdown. */
70
95
  const renderCursorRule = (rule, targetDir) => `---
71
96
  description: ${yamlString(rule.cursorDescription)}
@@ -104,6 +129,11 @@ exports.detectTools = detectTools;
104
129
  * directory the assistants actually read.
105
130
  */
106
131
  const findAssistantRoot = (dir, exists = node_fs_1.existsSync) => {
132
+ // Checked before the walk: `dir` is very often the repo root itself, and one level above it
133
+ // `~/.claude` looks exactly like a project asking for the rules.
134
+ if (exists((0, node_path_1.join)(dir, '.git'))) {
135
+ return null;
136
+ }
107
137
  let current = (0, node_path_1.dirname)(dir);
108
138
  for (;;) {
109
139
  if ((0, exports.detectTools)(current, exists).length) {
@@ -121,9 +151,20 @@ const findAssistantRoot = (dir, exists = node_fs_1.existsSync) => {
121
151
  }
122
152
  };
123
153
  exports.findAssistantRoot = findAssistantRoot;
124
- /** Parses and validates a `--tools` value. Throws on anything unknown rather than guessing. */
154
+ /**
155
+ * Parses and validates a `--tools` value. Throws on anything unknown, and on anything that names
156
+ * no tool at all — the CLI parser hands us whatever the shell gave it (`--tools ""` arrives as the
157
+ * number `0`, repeated `-t` flags as an array), and an explicit flag that wires nothing is far
158
+ * more likely a typo than an intent.
159
+ */
125
160
  const parseTools = (value) => {
126
- const requested = value
161
+ // `--tools ""` reaches us as the number 0, and a number never names an assistant.
162
+ const raw = Array.isArray(value)
163
+ ? value.join(',')
164
+ : typeof value === 'number'
165
+ ? ''
166
+ : String(value !== null && value !== void 0 ? value : '');
167
+ const requested = raw
127
168
  .split(',')
128
169
  .map((tool) => tool.trim().toLowerCase())
129
170
  .filter(Boolean);
@@ -131,6 +172,9 @@ const parseTools = (value) => {
131
172
  if (unknown.length) {
132
173
  throw new Error(`Unknown agent tool(s): ${unknown.join(', ')}. Supported: ${exports.AGENT_TOOLS.join(', ')}`);
133
174
  }
175
+ if (!requested.length) {
176
+ throw new Error(`--tools named no assistant. Supported: ${exports.AGENT_TOOLS.join(', ')}`);
177
+ }
134
178
  return exports.AGENT_TOOLS.filter((tool) => requested.includes(tool));
135
179
  };
136
180
  exports.parseTools = parseTools;
@@ -154,25 +198,70 @@ const planFiles = (dir, manifest, tools, ruleContents, read, exists = node_fs_1.
154
198
  });
155
199
  return;
156
200
  }
201
+ if (tool === 'claude') {
202
+ rules.forEach((rule) => {
203
+ planned.push({
204
+ path: `.claude/skills/${rule.name}/SKILL.md`,
205
+ contents: (0, exports.renderClaudeSkill)(rule, targetDir),
206
+ });
207
+ });
208
+ return;
209
+ }
157
210
  const entryFile = exports.TOOL_ENTRY_FILES[tool];
158
211
  const entryPath = (0, node_path_1.join)(dir, entryFile);
159
212
  const current = exists(entryPath) ? read(entryPath) : '';
160
- const body = tool === 'agents'
161
- ? (0, exports.renderPointerBlock)(rules, targetDir)
162
- : (0, exports.renderImportBlock)(rules, targetDir);
163
213
  try {
164
- planned.push({ path: entryFile, contents: (0, exports.upsertManagedBlock)(current, body) });
214
+ planned.push({
215
+ path: entryFile,
216
+ contents: (0, exports.upsertManagedBlock)(current, (0, exports.renderPointerBlock)(rules, targetDir)),
217
+ });
165
218
  }
166
219
  catch (error) {
167
220
  throw new Error(`${entryFile}: ${(0, exports.errorMessage)(error)}`);
168
221
  }
169
222
  });
223
+ // Written last so it can record every path above, itself included.
224
+ const statePath = `${targetDir}/${exports.STATE_FILE}`;
225
+ const state = {
226
+ version: manifest.version,
227
+ tools,
228
+ files: [...planned.map((file) => file.path), statePath].sort(),
229
+ };
230
+ planned.push({ path: statePath, contents: `${JSON.stringify(state, null, 2)}\n` });
170
231
  return planned;
171
232
  };
172
233
  exports.planFiles = planFiles;
234
+ /** The state recorded by the last install, or `null` when there is none to read. */
235
+ const readInstalledState = (dir, targetDir, read = (path) => (0, node_fs_1.readFileSync)(path, 'utf8'), exists = node_fs_1.existsSync) => {
236
+ const path = (0, node_path_1.join)(dir, targetDir, exports.STATE_FILE);
237
+ if (!exists(path)) {
238
+ return null;
239
+ }
240
+ try {
241
+ return JSON.parse(read(path));
242
+ }
243
+ catch (_a) {
244
+ // A corrupt state file must not block an install — the next write repairs it.
245
+ return null;
246
+ }
247
+ };
248
+ exports.readInstalledState = readInstalledState;
249
+ /**
250
+ * Files a previous install owned that the current manifest no longer produces — a rule renamed or
251
+ * retired upstream. Left in place, a stale `.mdc` keeps pointing Cursor at a withdrawn rule and
252
+ * agents keep following it, with `--check` none the wiser.
253
+ */
254
+ const findStale = (state, planned) => {
255
+ if (!(state === null || state === void 0 ? void 0 : state.files)) {
256
+ return [];
257
+ }
258
+ const current = new Set(planned.map((file) => file.path));
259
+ return state.files.filter((path) => !current.has(path));
260
+ };
261
+ exports.findStale = findStale;
173
262
  /** Planned files whose contents differ from what is on disk. */
174
263
  const findDrift = (dir, planned, read = (path) => (0, node_fs_1.readFileSync)(path, 'utf8'), exists = node_fs_1.existsSync) => planned.filter((file) => {
175
264
  const path = (0, node_path_1.join)(dir, file.path);
176
- return !exists(path) || read(path) !== file.contents;
265
+ return !exists(path) || (0, exports.normalizeEol)(read(path)) !== (0, exports.normalizeEol)(file.contents);
177
266
  });
178
267
  exports.findDrift = findDrift;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/genx",
3
3
  "description": "Genx CLI",
4
- "version": "15.35.0",
4
+ "version": "15.35.1",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "engines": {
7
7
  "node": ">=22.0.0"
@@ -18,20 +18,20 @@
18
18
  "genx": "./bin/genx"
19
19
  },
20
20
  "dependencies": {
21
- "@genesislcap/build-kit": "15.35.0",
22
- "@genesislcap/eslint-stylelint-builder": "15.35.0",
23
- "@genesislcap/event-type-codegen": "15.35.0",
24
- "@genesislcap/rollup-builder": "15.35.0",
25
- "@genesislcap/ts-builder": "15.35.0",
26
- "@genesislcap/uvu-playwright-builder": "15.35.0",
27
- "@genesislcap/vite-builder": "15.35.0",
28
- "@genesislcap/webpack-builder": "15.35.0",
21
+ "@genesislcap/build-kit": "15.35.1",
22
+ "@genesislcap/eslint-stylelint-builder": "15.35.1",
23
+ "@genesislcap/event-type-codegen": "15.35.1",
24
+ "@genesislcap/rollup-builder": "15.35.1",
25
+ "@genesislcap/ts-builder": "15.35.1",
26
+ "@genesislcap/uvu-playwright-builder": "15.35.1",
27
+ "@genesislcap/vite-builder": "15.35.1",
28
+ "@genesislcap/webpack-builder": "15.35.1",
29
29
  "cac": "^6.7.14",
30
30
  "consola": "^3.0.2",
31
31
  "serve-handler": "^6.1.5"
32
32
  },
33
33
  "devDependencies": {
34
- "@genesislcap/foundation-testing": "15.35.0"
34
+ "@genesislcap/foundation-testing": "15.35.1"
35
35
  },
36
36
  "repository": {
37
37
  "type": "git",