@tsfpp/agents 1.2.3 → 1.3.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.
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: test-standard
3
+ description: >
4
+ Normative TSF++ testing rules and idioms for all test files. Load when writing
5
+ or reviewing *.test.ts or *.test.tsx files: toolchain per layer (Vitest,
6
+ fast-check, RTL, MSW, testcontainers), AAA structure, property-based test
7
+ patterns, in-memory port stubs, factory conventions, coverage requirements,
8
+ and forbidden patterns (data-testid, vi.fn() for ports, snapshot tests,
9
+ implementation assertions). Load alongside prelude-api when writing core tests.
10
+ ---
11
+
12
+ # TSF++ test standard
13
+
14
+ Full standard: `node_modules/@tsfpp/standard/spec/TEST_CODING_STANDARD.md`
15
+
16
+ All base TSF++ rules apply to test code. No `any`, no `let`, no forbidden constructs.
17
+
18
+ ---
19
+
20
+ ## Toolchain per layer
21
+
22
+ | Layer | Runner | Property tests | Network | DB |
23
+ |---|---|---|---|---|
24
+ | Core | Vitest | fast-check — required | — | — |
25
+ | Use-case | Vitest | fast-check — optional | — | In-memory stub |
26
+ | API / handler | Vitest | — | MSW | In-memory stub |
27
+ | DAL | Vitest | — | — | Real / containerised |
28
+ | React | Vitest + RTL | — | MSW | — |
29
+
30
+ ---
31
+
32
+ ## Structure — AAA
33
+
34
+ ```ts
35
+ it('returns None when the input string is empty', () => {
36
+ const raw = '' // Arrange
37
+
38
+ const result = mkTrackId(raw) // Act
39
+
40
+ expect(result).toEqual(none) // Assert
41
+ })
42
+ ```
43
+
44
+ One blank line between phases. One logical assertion concept per test.
45
+ Test descriptions are full sentences describing behaviour — never implementation echoes.
46
+
47
+ ```ts
48
+ // Good
49
+ it('returns None when the input string is empty')
50
+ it('responds with 422 when title is missing')
51
+
52
+ // Bad
53
+ it('mkTrackId empty')
54
+ it('handler validation test')
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Describe block structure
60
+
61
+ ```ts
62
+ describe('mkTrackId', () => {
63
+ describe('when the input is valid', () => {
64
+ it('returns Some containing a branded TrackId', () => { ... })
65
+ })
66
+ describe('when the input is empty', () => {
67
+ it('returns None', () => { ... })
68
+ })
69
+ })
70
+ ```
71
+
72
+ Max two levels of nesting. No branching or loops in test bodies.
73
+
74
+ ---
75
+
76
+ ## Property-based tests — fast-check
77
+
78
+ Required for every pure function and every `@law` in JSDoc.
79
+
80
+ ```ts
81
+ import * as fc from 'fast-check'
82
+
83
+ // Specific case
84
+ it('returns Some for a non-empty string', () => {
85
+ expect(isSome(mkTrackId('abc'))).toBe(true)
86
+ })
87
+
88
+ // Law — holds for all inputs
89
+ it('satisfies: any non-empty string is accepted', () => {
90
+ fc.assert(
91
+ fc.property(fc.string({ minLength: 1 }), (s) => {
92
+ expect(isSome(mkTrackId(s))).toBe(true)
93
+ }),
94
+ )
95
+ })
96
+
97
+ // Result identity law
98
+ it('satisfies map(id) ≡ id', () => {
99
+ fc.assert(
100
+ fc.property(fc.integer(), (n) => {
101
+ expect(pipe(ok(n), map(x => x))).toEqual(ok(n))
102
+ }),
103
+ )
104
+ })
105
+ ```
106
+
107
+ ---
108
+
109
+ ## React components — RTL
110
+
111
+ ```ts
112
+ import { render, screen } from '@testing-library/react'
113
+ import userEvent from '@testing-library/user-event'
114
+
115
+ it('calls onSelect with the track id when clicked', async () => {
116
+ const onSelect = vi.fn()
117
+ render(<TrackCard track={makeTrack()} onSelect={some(onSelect)} />)
118
+
119
+ await userEvent.click(screen.getByRole('article'))
120
+
121
+ expect(onSelect).toHaveBeenCalledWith(expect.any(String))
122
+ })
123
+ ```
124
+
125
+ Query hierarchy — use the first that works:
126
+ 1. `getByRole`
127
+ 2. `getByLabelText`
128
+ 3. `getByText`
129
+ 4. `getByPlaceholderText`
130
+
131
+ Never `getByTestId`.
132
+
133
+ ---
134
+
135
+ ## Network — MSW
136
+
137
+ ```ts
138
+ import { http, HttpResponse } from 'msw'
139
+ import { setupServer } from 'msw/node'
140
+
141
+ const server = setupServer(
142
+ http.get('/api/tracks', () => HttpResponse.json(fixtures)),
143
+ )
144
+
145
+ beforeAll(() => server.listen())
146
+ afterEach(() => server.resetHandlers())
147
+ afterAll(() => server.close())
148
+ ```
149
+
150
+ Never stub `fetch`, `axios`, or any HTTP client directly.
151
+
152
+ ---
153
+
154
+ ## Port stubs — in-memory only
155
+
156
+ ```ts
157
+ // Good — typed in-memory implementation
158
+ const repo = mkInMemoryTrackRepository()
159
+
160
+ // Bad — partial vi.fn() mock
161
+ const repo = { findById: vi.fn().mockResolvedValue(track) }
162
+ ```
163
+
164
+ `vi.fn()` is permitted only for standalone callbacks (`onClose`, `onSelect`, etc.).
165
+
166
+ ---
167
+
168
+ ## API / handler tests
169
+
170
+ ```ts
171
+ it('responds with 201 and a Location header on valid input', async () => {
172
+ const req = new Request('http://localhost/v1/tracks', {
173
+ method: 'POST',
174
+ body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
175
+ headers: { 'Content-Type': 'application/json' },
176
+ })
177
+
178
+ const res = await handler(req)
179
+
180
+ expect(res.status).toBe(201)
181
+ expect(res.headers.get('Location')).toMatch(/\/v1\/tracks\//)
182
+ })
183
+
184
+ it('responds with 422 when title is missing', async () => {
185
+ const req = new Request('http://localhost/v1/tracks', {
186
+ method: 'POST',
187
+ body: JSON.stringify({ artistId: 'a1' }),
188
+ headers: { 'Content-Type': 'application/json' },
189
+ })
190
+
191
+ const res = await handler(req)
192
+
193
+ expect(res.status).toBe(422)
194
+ })
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Factories
200
+
201
+ ```ts
202
+ // tests/factories/track.factory.ts
203
+ const makeTrack = (overrides: Partial<Track> = {}): Track => ({
204
+ id: mkTrackId('test-track-001'),
205
+ title: 'Default Title',
206
+ artistId: mkArtistId('test-artist-001'),
207
+ ...overrides,
208
+ })
209
+ ```
210
+
211
+ - Factories live in `tests/factories/` — never inline raw object literals
212
+ - IDs are deterministic strings that cannot collide with real data
213
+ - Never copy IDs from production or staging
214
+
215
+ ---
216
+
217
+ ## Coverage requirements
218
+
219
+ - Every public export has at least one test for the primary success case
220
+ - Every error path (`Err`, `None`, non-2xx) has a corresponding test
221
+ - Every branch, switch case, and ternary arm is exercised
222
+ - Minimum enforced: 80 % statements, 80 % branches per package
223
+
224
+ ---
225
+
226
+ ## Never
227
+
228
+ | Forbidden | Use instead |
229
+ |---|---|
230
+ | `getByTestId` | `getByRole`, `getByLabelText`, `getByText` |
231
+ | `vi.fn()` for a port interface | In-memory implementation |
232
+ | Assert internal function was called | Assert observable outcome |
233
+ | `any` in test code | Typed fixtures and factories |
234
+ | `setTimeout` delays | `waitFor` / `findBy*` |
235
+ | `beforeAll` for mutable state | `beforeEach` |
236
+ | Snapshot tests for components or API shape | Explicit assertions |
237
+ | Shallow rendering | RTL full render |
238
+ | Access non-exported symbols | Test through the public API |
package/init.mjs CHANGED
@@ -9,7 +9,6 @@
9
9
  * Usage:
10
10
  * pnpm dlx @tsfpp/agents (one-shot, no install)
11
11
  * node node_modules/@tsfpp/agents/init.mjs
12
- * node node_modules/@tsfpp/agents/init.mjs --yes (overwrite all without prompting)
13
12
  */
14
13
 
15
14
  import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
@@ -21,8 +20,6 @@ import { createInterface } from 'node:readline';
21
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
21
  const cwd = process.cwd();
23
22
 
24
- const yes = process.argv.includes('--yes') || process.argv.includes('-y');
25
-
26
23
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
27
24
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
28
25
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
@@ -33,37 +30,36 @@ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
33
30
 
34
31
  const FILES = [
35
32
  // Always-on workspace instructions
36
- ['copilot/copilot-instructions.md', '.github/copilot-instructions.md'],
33
+ ['copilot/copilot-instructions.md', '.github/copilot-instructions.md'],
37
34
 
38
35
  // Scoped instruction files
39
- ['copilot/instructions/tsfpp-base.instructions.md', '.github/instructions/tsfpp-base.instructions.md'],
40
- ['copilot/instructions/tsfpp-react.instructions.md', '.github/instructions/tsfpp-react.instructions.md'],
41
- ['copilot/instructions/tsfpp-api.instructions.md', '.github/instructions/tsfpp-api.instructions.md'],
42
- ['copilot/instructions/tsfpp-prelude.instructions.md', '.github/instructions/tsfpp-prelude.instructions.md'],
36
+ ['copilot/instructions/tsfpp-base.instructions.md', '.github/instructions/tsfpp-base.instructions.md'],
37
+ ['copilot/instructions/tsfpp-prelude.instructions.md', '.github/instructions/tsfpp-prelude.instructions.md'],
38
+ ['copilot/instructions/tsfpp-api.instructions.md', '.github/instructions/tsfpp-api.instructions.md'],
39
+ ['copilot/instructions/tsfpp-react.instructions.md', '.github/instructions/tsfpp-react.instructions.md'],
40
+ ['copilot/instructions/tsfpp-testing.instructions.md', '.github/instructions/tsfpp-testing.instructions.md'],
41
+ ['copilot/instructions/trunk.instructions.md', '.github/instructions/trunk.instructions.md'],
43
42
 
44
43
  // Agents
45
- ['copilot/agents/tsfpp-guarded-coding.agent.md', '.github/agents/tsfpp-guarded-coding.agent.md'],
46
- ['copilot/agents/tsfpp-audit.agent.md', '.github/agents/tsfpp-audit.agent.md'],
47
- ['copilot/agents/tsfpp-refactor-engineer.agent.md', '.github/agents/tsfpp-refactor-engineer.agent.md'],
48
- ['copilot/agents/tsfpp-annotate.agent.md', '.github/agents/tsfpp-annotate.agent.md'],
49
- ['copilot/agents/trunk-enforcer.agent.md', '.github/agents/trunk-enforcer.agent.md'],
50
- ['copilot/agents/trunk-release.agent.md', '.github/agents/trunk-release.agent.md'],
51
-
52
- // Trunk workflow instructions
53
- ['copilot/instructions/trunk.instructions.md', '.github/instructions/trunk.instructions.md'],
44
+ ['copilot/agents/tsfpp-tdd.agent.md', '.github/agents/tsfpp-tdd.agent.md'],
45
+ ['copilot/agents/tsfpp-guarded-coding.agent.md', '.github/agents/tsfpp-guarded-coding.agent.md'],
46
+ ['copilot/agents/tsfpp-audit.agent.md', '.github/agents/tsfpp-audit.agent.md'],
47
+ ['copilot/agents/tsfpp-refactor-engineer.agent.md', '.github/agents/tsfpp-refactor-engineer.agent.md'],
48
+ ['copilot/agents/tsfpp-annotate.agent.md', '.github/agents/tsfpp-annotate.agent.md'],
54
49
 
55
50
  // Reusable prompts
56
- ['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
57
- ['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
51
+ ['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
52
+ ['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
58
53
 
59
54
  // Reusable skills
60
- ['copilot/skills/coding-standard/SKILL.md', '.github/skills/coding-standard/SKILL.md'],
61
- ['copilot/skills/prelude-api/SKILL.md', '.github/skills/prelude-api/SKILL.md'],
62
- ['copilot/skills/boundary-api/SKILL.md', '.github/skills/boundary-api/SKILL.md'],
63
- ['copilot/skills/react-coding-standard/SKILL.md', '.github/skills/react-coding-standard/SKILL.md'],
55
+ ['copilot/skills/coding-standard/SKILL.md', '.github/skills/coding-standard/SKILL.md'],
56
+ ['copilot/skills/prelude-api/SKILL.md', '.github/skills/prelude-api/SKILL.md'],
57
+ ['copilot/skills/boundary-api/SKILL.md', '.github/skills/boundary-api/SKILL.md'],
58
+ ['copilot/skills/react-coding-standard/SKILL.md', '.github/skills/react-coding-standard/SKILL.md'],
59
+ ['copilot/skills/test-standard/SKILL.md', '.github/skills/test-standard/SKILL.md'],
64
60
 
65
61
  // Claude Code
66
- ['claude/CLAUDE.md', '.claude/CLAUDE.md'],
62
+ ['claude/CLAUDE.md', '.claude/CLAUDE.md'],
67
63
  ];
68
64
 
69
65
  // ─── ESLint config generation ─────────────────────────────────────────────────
@@ -99,9 +95,7 @@ async function askProfile(label) {
99
95
  console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
100
96
  console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
101
97
  console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
102
- console.log(` ${dim('n')} skip ${dim('— keep existing / do not generate')}`);
103
- const choice = await ask(` ${dim('[1/2/3/n, default: 1]')} `);
104
- if (choice === 'n') return null;
98
+ const choice = await ask(` ${dim('[1/2/3, default: 1]')} `);
105
99
  return choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
106
100
  }
107
101
 
@@ -148,7 +142,7 @@ function generateSingleConfig(profile) {
148
142
  return `${imp}\nexport default [...${spread}]\n`;
149
143
  }
150
144
 
151
- async function writeEslintConfig(results) {
145
+ async function writeEslintConfig() {
152
146
  const packages = await detectWorkspacePackages();
153
147
 
154
148
  let content;
@@ -160,17 +154,12 @@ async function writeEslintConfig(results) {
160
154
  for (const pkg of packages) {
161
155
  packageProfiles[pkg] = await askProfile(pkg);
162
156
  }
163
- const activeProfiles = Object.fromEntries(
164
- Object.entries(packageProfiles).filter(([, p]) => p !== null)
165
- );
166
- if (Object.keys(activeProfiles).length === 0) return;
167
- content = generateMonorepoConfig(activeProfiles);
157
+ content = generateMonorepoConfig(packageProfiles);
168
158
  description = 'monorepo';
169
159
  } else {
170
160
  const profile = await askProfile('this project');
171
- if (profile === null) return;
172
- content = generateSingleConfig(profile);
173
- description = `profile: ${profile}`;
161
+ content = generateSingleConfig(profile);
162
+ description = `profile: ${profile}`;
174
163
  }
175
164
 
176
165
  try {
@@ -200,23 +189,16 @@ async function ask(question) {
200
189
  }
201
190
 
202
191
  async function confirm(question) {
203
- if (yes) return true;
204
192
  return (await ask(question)) === 'y';
205
193
  }
206
194
 
207
195
  // ─── Main ─────────────────────────────────────────────────────────────────────
208
196
 
209
- process.on('SIGINT', () => {
210
- console.log('\n\n Aborted.\n');
211
- process.exit(0);
212
- });
213
-
214
- async function main() {
215
- console.log();
216
- console.log(bold(' @tsfpp/agents — init'));
217
- console.log(dim(' Sets up Copilot agents, instructions, prompts, skills, and ESLint config.\n'));
197
+ console.log();
198
+ console.log(bold(' @tsfpp/agents — init'));
199
+ console.log(dim(' Sets up Copilot agents, instructions, prompts, skills, and ESLint config.\n'));
218
200
 
219
- const results = { copied: [], skipped: [], failed: [] };
201
+ const results = { copied: [], skipped: [], failed: [] };
220
202
 
221
203
  // ── Copy files ────────────────────────────────────────────────────────────────
222
204
 
@@ -253,32 +235,45 @@ console.log();
253
235
  const eslintDest = join(cwd, 'eslint.config.js');
254
236
 
255
237
  if (existsSync(eslintDest)) {
256
- if (yes) {
238
+ const overwrite = await confirm(
239
+ ` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
240
+ );
241
+ if (!overwrite) {
257
242
  results.skipped.push('eslint.config.js');
258
- console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped — project-managed)')}`);
243
+ console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
259
244
  } else {
260
- const overwrite = await confirm(
261
- ` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
262
- );
263
- if (overwrite) await writeEslintConfig(results);
264
- else {
265
- results.skipped.push('eslint.config.js');
266
- console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
267
- }
245
+ await writeEslintConfig();
268
246
  }
269
247
  } else {
270
- await writeEslintConfig(results);
248
+ await writeEslintConfig();
271
249
  }
272
250
 
251
+ async function writeEslintConfig() {
252
+ console.log(` Which ESLint profile does this project use?`);
253
+ console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
254
+ console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
255
+ console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
273
256
 
257
+ const choice = await ask(` ${dim('[1/2/3, default: 1]')} `);
258
+ const profile = choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
259
+
260
+ try {
261
+ await writeFile(eslintDest, ESLINT_PROFILES[profile], 'utf8');
262
+ results.copied.push('eslint.config.js');
263
+ console.log(` ${green('✓')} eslint.config.js ${dim(`(profile: ${profile})`)}`);
264
+ } catch (err) {
265
+ results.failed.push('eslint.config.js');
266
+ console.log(` \x1b[31m✗\x1b[0m eslint.config.js ${dim(`(${err.message})`)}`);
267
+ }
268
+ }
274
269
 
275
270
  // ── Generate tsconfig.json ────────────────────────────────────────────────────
276
271
 
277
272
  console.log();
278
273
 
279
- await writeTsConfigs(await detectWorkspacePackages(), results);
274
+ await writeTsConfigs(await detectWorkspacePackages());
280
275
 
281
- async function writeTsConfigs(packages, results) {
276
+ async function writeTsConfigs(packages) {
282
277
  const PRESETS = {
283
278
  app: { extends: '@tsfpp/tsconfig/app', label: 'app — application / tool (noEmit)' },
284
279
  lib: { extends: '@tsfpp/tsconfig/lib', label: 'lib — publishable package (declaration, composite)' },
@@ -288,9 +283,7 @@ async function writeTsConfigs(packages, results) {
288
283
  console.log(`\n tsconfig preset for ${bold(label)}:`);
289
284
  console.log(` ${dim('1')} app ${dim('— application / tool (noEmit: true)')}`);
290
285
  console.log(` ${dim('2')} lib ${dim('— publishable package (declaration, composite)')}`);
291
- console.log(` ${dim('n')} skip ${dim('— keep existing / do not generate')}`);
292
- const choice = await ask(` ${dim('[1/2/n, default: 1]')} `);
293
- if (choice === 'n') return null;
286
+ const choice = await ask(` ${dim('[1/2, default: 1]')} `);
294
287
  return choice === '2' ? 'lib' : 'app';
295
288
  }
296
289
 
@@ -311,11 +304,6 @@ async function writeTsConfigs(packages, results) {
311
304
 
312
305
  async function writeIfConfirmed(destPath, content, label) {
313
306
  if (existsSync(destPath)) {
314
- if (yes) {
315
- results.skipped.push(label);
316
- console.log(` ${dim('–')} ${dim(label)} ${dim('(skipped — project-managed)')}`);
317
- return;
318
- }
319
307
  const overwrite = await confirm(
320
308
  ` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
321
309
  );
@@ -345,7 +333,6 @@ async function writeTsConfigs(packages, results) {
345
333
  }
346
334
 
347
335
  for (const [pkg, preset] of Object.entries(packagePresets)) {
348
- if (preset === null) continue;
349
336
  const destPath = join(cwd, pkg, 'tsconfig.json');
350
337
  const content = generateTsConfig(preset);
351
338
  await writeIfConfirmed(destPath, content, `${pkg}/tsconfig.json`);
@@ -357,26 +344,22 @@ async function writeTsConfigs(packages, results) {
357
344
  await writeIfConfirmed(rootDest, rootContent, 'tsconfig.json (root references)');
358
345
  } else {
359
346
  // Single package
360
- const preset = await askPreset('this project');
361
- if (preset === null) return;
347
+ const preset = await askPreset('this project');
362
348
  const dest = join(cwd, 'tsconfig.json');
363
349
  const content = generateTsConfig(preset);
364
350
  await writeIfConfirmed(dest, content, 'tsconfig.json');
365
351
  }
366
352
  }
367
353
 
368
- console.log();
369
- console.log(dim(' ─────────────────────────────────────────'));
370
- console.log(` ${green(results.copied.length + ' copied')} ${yellow(results.skipped.length + ' skipped')} ${results.failed.length > 0 ? `\x1b[31m${results.failed.length} failed\x1b[0m` : dim('0 failed')}`);
371
- console.log();
354
+ console.log();
355
+ console.log(dim(' ─────────────────────────────────────────'));
356
+ console.log(` ${green(results.copied.length + ' copied')} ${yellow(results.skipped.length + ' skipped')} ${results.failed.length > 0 ? `\x1b[31m${results.failed.length} failed\x1b[0m` : dim('0 failed')}`);
357
+ console.log();
372
358
 
373
- if (results.failed.length === 0) {
374
- console.log(' ' + bold('Done.') + ' Reload VS Code to activate Copilot instructions.');
375
- console.log(dim(' Commit the generated files — they are workspace configuration.\n'));
376
- } else {
377
- console.log(' Some files could not be copied. Check the errors above.\n');
378
- process.exit(1);
379
- }
359
+ if (results.failed.length === 0) {
360
+ console.log(' ' + bold('Done.') + ' Reload VS Code to activate Copilot instructions.');
361
+ console.log(dim(' Commit the generated files — they are workspace configuration.\n'));
362
+ } else {
363
+ console.log(' Some files could not be copied. Check the errors above.\n');
364
+ process.exit(1);
380
365
  }
381
-
382
- main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsfpp/agents",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "Workspace AI tooling for TSF++ projects: scoped instructions, coding agents, and reusable prompts",
5
5
  "keywords": [
6
6
  "tsfpp",