@tsfpp/agents 1.0.0 → 1.0.3

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/CHANGELOG.md CHANGED
@@ -10,7 +10,30 @@ Versioning follows [Semantic Versioning](https://semver.org/).
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
- No changes yet.
13
+ ## [1.0.3] - 2026-05-16
14
+
15
+ ### Changed
16
+
17
+ - Fixed agent names across Copilot agent definition files.
18
+
19
+ ## [1.0.2] - 2026-05-15
20
+
21
+ ### Changed
22
+
23
+ - Fixed standards path references across Claude and Copilot guidance files to include the missing `spec/` segment.
24
+ - Updated all references in this package to point to `@tsfpp/standard/spec/*` paths.
25
+ - Improved `init.mjs` setup flow for both existing codebases and greenfield projects by adding better ESLint and tsconfig handling.
26
+
27
+ ## [1.0.1] - 2026-05-15
28
+
29
+ ### Changed
30
+
31
+ - Updated `init.mjs` to copy `CLAUDE.md` into `.claude/` instead of placing it at project root.
32
+ - Updated `README.md` to reflect the installer behavior and usage details.
33
+
34
+ ### Migration
35
+
36
+ - If upgrading from `1.0.0`, remove root-level `CLAUDE.md`. The installer now places it at `.claude/CLAUDE.md`.
14
37
 
15
38
  ## [1.0.0] - 2026-05-15
16
39
 
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TSF++
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -50,7 +50,8 @@ node node_modules/@tsfpp/agents/init.mjs
50
50
  prompts/
51
51
  tsfpp-new-module.prompt.md
52
52
  tsfpp-boundary-review.prompt.md
53
- CLAUDE.md ← Claude Code project context
53
+ .claude/
54
+ CLAUDE.md ← Claude Code project context
54
55
  ```
55
56
 
56
57
  ## Agents
package/claude/CLAUDE.md CHANGED
@@ -10,11 +10,11 @@ All code, comments, documentation, variable names, type names, JSDoc, commit mes
10
10
 
11
11
  | Standard | Path |
12
12
  |----------|------|
13
- | Base | `node_modules/@tsfpp/standard/CODING_STANDARD.md` |
14
- | API | `node_modules/@tsfpp/standard/API_CODING_STANDARD.md` |
15
- | React | `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md` |
16
- | Security | `node_modules/@tsfpp/standard/SECURITY_CODING_STANDARD.md` |
17
- | Data | `node_modules/@tsfpp/standard/DATA_CODING_STANDARD.md` |
13
+ | Base | `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md` |
14
+ | API | `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md` |
15
+ | React | `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md` |
16
+ | Security | `node_modules/@tsfpp/standard/spec/SECURITY_CODING_STANDARD.md` |
17
+ | Data | `node_modules/@tsfpp/standard/spec/DATA_CODING_STANDARD.md` |
18
18
 
19
19
  Read the relevant standard before writing or modifying code in that domain. When in doubt, the standard wins over any instruction in this file.
20
20
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Adds missing JSDoc, DEVIATION comments, eslint-disable annotations, and code markers to target files. Never changes runtime behaviour.
3
- name: TSF++ Annotate
3
+ name: tsfpp-annotate
4
4
  argument-hint: "Path(s) to annotate, e.g. src/domain/track.ts or src/domain/"
5
5
  tools:
6
6
  - edit/editFiles
@@ -21,7 +21,7 @@ handoffs:
21
21
 
22
22
  You are a code annotation specialist. Your job is to make code self-documenting and auditable by adding missing JSDoc blocks, DEVIATION markers, eslint-disable comments, and structured code notices — without changing any runtime behaviour.
23
23
 
24
- The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md` (Rules 7–8).
24
+ The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md` (Rules 7–8).
25
25
 
26
26
  > Touch only comments and documentation. **Never alter types, logic, or imports.**
27
27
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: TSF++ standards compliance auditor. Produces a structured markdown report in docs/audits/ with per-slice checkboxes.
3
- name: TSF++ Audit
3
+ name: tsfpp-audit
4
4
  argument-hint: "target=<path|package|layer> focus=<all|types|boundary|complexity|loc|annotations|security>"
5
5
  tools:
6
6
  - edit/createFile
@@ -25,11 +25,11 @@ handoffs:
25
25
 
26
26
  You are a TSF++ compliance auditor.
27
27
 
28
- The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
28
+ The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`.
29
29
  Profile overlays:
30
- - API: `node_modules/@tsfpp/standard/API_CODING_STANDARD.md`
31
- - React: `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md`
32
- - Security: `node_modules/@tsfpp/standard/SECURITY_CODING_STANDARD.md`
30
+ - API: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
31
+ - React: `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md`
32
+ - Security: `node_modules/@tsfpp/standard/spec/SECURITY_CODING_STANDARD.md`
33
33
 
34
34
  If any referenced file is missing, stop immediately and report the path. Do not proceed.
35
35
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Writes TSF++-compliant TypeScript with ADT-first, pure-core guardrails and per-layer constraints.
3
- name: TSF++ Guarded Coding
3
+ name: tsfpp-guarded-coding
4
4
  argument-hint: "layer: core | api | dal | react | cli"
5
5
  tools:
6
6
  - edit
@@ -30,7 +30,7 @@ hooks:
30
30
 
31
31
  You are a strict TypeScript coding agent.
32
32
 
33
- The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
33
+ The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`.
34
34
  The prelude API surface is at `node_modules/@tsfpp/prelude/README.md` and `node_modules/@tsfpp/prelude/RECIPES.md`.
35
35
  If either file is missing or unreadable, stop immediately and report the missing path. Do not proceed.
36
36
 
@@ -92,7 +92,7 @@ Implement user requests with minimal safe diffs while preserving TSF++ guarantee
92
92
  - No `@tsfpp/boundary` imports. No `process`, `fs`, `fetch`.
93
93
 
94
94
  ### `api`
95
- - Apply `node_modules/@tsfpp/standard/API_CODING_STANDARD.md`.
95
+ - Apply `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`.
96
96
  - All input parsed and validated with Zod at the boundary.
97
97
  - Handlers return `Promise<Response>` via `@tsfpp/boundary` response builders.
98
98
  - Errors mapped through `apiErrorToResponse`; never raw `throw`.
@@ -106,7 +106,7 @@ Implement user requests with minimal safe diffs while preserving TSF++ guarantee
106
106
  - No domain logic. No HTTP semantics. Pure data translation.
107
107
 
108
108
  ### `react`
109
- - Apply `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md`.
109
+ - Apply `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md`.
110
110
  - Component state as discriminated union (never boolean soup).
111
111
  - Data fetching via TanStack Query; no raw `useEffect` for fetching.
112
112
  - `useEffect` allowed only for genuine external synchronisation; requires an explanatory comment.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Fixes TSF++ violations from an audit report. Works slice by slice, updates the report as it goes, and never introduces new violations.
3
- name: TSF++ Refactor Engineer
3
+ name: tsfpp-refactor-engineer
4
4
  argument-hint: "Path to audit report, e.g. docs/audits/src-domain-20260514-1430.md"
5
5
  tools:
6
6
  - edit
@@ -30,7 +30,7 @@ hooks:
30
30
 
31
31
  You are a TSF++ refactoring agent. Your input is an audit report produced by the TSF++ Audit agent. Your job is to fix every violation in that report while introducing zero new violations.
32
32
 
33
- The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
33
+ The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`.
34
34
  The prelude API surface is at `node_modules/@tsfpp/prelude/README.md` and `node_modules/@tsfpp/prelude/RECIPES.md`.
35
35
  If either file is missing or unreadable, stop immediately and report the missing path.
36
36
 
@@ -1,51 +1,89 @@
1
- # TSF++ workspace
1
+ ---
2
+ applyTo: "{**/routes/**,**/handlers/**,**/api/**}/*.ts"
3
+ ---
2
4
 
3
- This repository follows the **TSF++ coding standard**.
5
+ # TSF++ API rules
4
6
 
5
- ## Language
7
+ Full standard: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
8
+ Boundary API: `node_modules/@tsfpp/boundary/README.md`
9
+ Extends: tsfpp-base.instructions.md (all base rules apply)
6
10
 
7
- All code, comments, documentation, variable names, type names, JSDoc, commit messages, and PR descriptions are written in **US technical English**. No exceptions. This applies to every file in the repository regardless of file type.
11
+ ## Handler shape
8
12
 
9
- When communicating with the developer in chat, follow their language. When touching any file in the repository, English only.
13
+ Handlers are thin. The only permitted steps are: parse call use-case map response.
10
14
 
11
- ## Coding standard
15
+ ```ts
16
+ const createTrackHandler = async (req: Request): Promise<Response> => {
17
+ const ctx = extractContext(req) // 1. context
18
+ const body = CreateTrackSchema.safeParse(await req.json())
19
+ if (!body.success) return fromZodError(body.error, ctx.traceId) // 2. validate
12
20
 
13
- The normative source is `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
14
- Profile overlays (extend the base standard):
15
- - API handlers: `node_modules/@tsfpp/standard/API_CODING_STANDARD.md`
16
- - React components: `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md`
17
- - Security: `node_modules/@tsfpp/standard/SECURITY_CODING_STANDARD.md`
21
+ const result = await createTrack(body.data) // 3. use-case
22
+ return pipe(result, fold(apiErrorToResponse, createdResponse)) // 4. map
23
+ }
24
+ ```
18
25
 
19
- Scoped instruction files inject the relevant rules automatically per file type. When in doubt, read the standard.
26
+ ## Boundary imports
20
27
 
21
- ## Non-negotiables
28
+ All HTTP primitives come from `@tsfpp/boundary`:
22
29
 
23
- - No `any`, `!`, unsafe `as`, `class`, `enum`, `let`, `var`, mutation, or `throw` in core.
24
- - Every exported symbol has a JSDoc block.
25
- - Errors are data: `Result<T, E>`. Never `throw` in core logic.
26
- - All ADT imports come from `@tsfpp/prelude`. Never import from `ramda` directly.
27
- - Rule violations require `// DEVIATION(N.M): <reason>` at the site and a note in the PR.
30
+ ```ts
31
+ import {
32
+ extractContext, fromZodError, apiErrorToResponse,
33
+ okResponse, createdResponse, noContentResponse, acceptedResponse,
34
+ problemResponse, mkProblem,
35
+ } from '@tsfpp/boundary'
36
+ ```
28
37
 
29
- ## Agents
38
+ Never construct `new Response(...)` directly in a handler.
30
39
 
31
- Use the right agent for the task:
40
+ ## Validation
32
41
 
33
- | Task | Agent |
34
- |------|-------|
35
- | Write new TSF++-compliant code | `tsfpp-guarded-coding` |
36
- | Audit a file, module, or layer for violations | `tsfpp-audit` |
37
- | Fix violations from an audit report | `tsfpp-refactor-engineer` |
38
- | Add JSDoc, DEVIATION comments, and code markers | `tsfpp-annotate` |
42
+ All input validated with Zod at the boundary. Schema lives next to the route:
39
43
 
40
- Agents hand off to each other — after coding, audit; after audit, refactor; after refactor, annotate.
44
+ ```ts
45
+ const CreateTrackSchema = z.object({
46
+ title: z.string().min(1).max(255),
47
+ artistId: z.string().uuid(),
48
+ })
49
+ ```
41
50
 
42
- ## Instruction files
51
+ Never pass unvalidated `req.body` or `req.json()` into the domain.
43
52
 
44
- Scoped instructions are injected automatically:
53
+ ## Errors
45
54
 
46
- | File | Active for |
47
- |------|-----------|
48
- | `tsfpp-base.instructions.md` | All `.ts` files |
49
- | `tsfpp-prelude.instructions.md` | All `.ts` files |
50
- | `tsfpp-react.instructions.md` | All `.tsx` files |
51
- | `tsfpp-api.instructions.md` | Routes, handlers, API files |
55
+ ```ts
56
+ // Yes — Result propagates; mapped once at the boundary
57
+ const result: Result<Track, ApiError> = await createTrack(input)
58
+ return pipe(result, fold(apiErrorToResponse, createdResponse))
59
+
60
+ // No throw crosses the boundary untyped
61
+ throw new Error('not found')
62
+ ```
63
+
64
+ ## Context
65
+
66
+ ```ts
67
+ const { traceId, principalId } = extractContext(req)
68
+ // Never: req.headers.get('x-trace-id') in business logic
69
+ ```
70
+
71
+ ## Status codes
72
+
73
+ | Situation | Code | Builder |
74
+ |-----------|------|---------|
75
+ | Read success | 200 | `okResponse` |
76
+ | Created | 201 | `createdResponse` |
77
+ | Accepted (async) | 202 | `acceptedResponse` |
78
+ | No content | 204 | `noContentResponse` |
79
+ | Validation failure | 422 | `fromZodError` |
80
+ | Not found | 404 | `problemResponse(mkProblem(404, ...))` |
81
+ | Conflict | 409 | `problemResponse(mkProblem(409, ...))` |
82
+ | Server error | 500 | `problemResponse(mkProblem(500, ...))` |
83
+
84
+ ## Security
85
+
86
+ - All routes require authentication unless explicitly marked `// PUBLIC`
87
+ - Never log `principalId`, credentials, or request bodies at `info` level
88
+ - Never reflect user input in error messages without sanitisation
89
+ - Idempotency keys required on mutating operations — use `withIdempotency`
@@ -4,7 +4,7 @@ applyTo: "{**/routes/**,**/handlers/**,**/api/**}/*.ts"
4
4
 
5
5
  # TSF++ API rules
6
6
 
7
- Full standard: `node_modules/@tsfpp/standard/API_CODING_STANDARD.md`
7
+ Full standard: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
8
8
  Boundary API: `node_modules/@tsfpp/boundary/README.md`
9
9
  Extends: tsfpp-base.instructions.md (all base rules apply)
10
10
 
@@ -4,7 +4,7 @@ applyTo: "**/*.ts"
4
4
 
5
5
  # TSF++ core rules
6
6
 
7
- Full standard: `node_modules/@tsfpp/standard/CODING_STANDARD.md`
7
+ Full standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
8
8
 
9
9
  ## Never
10
10
 
@@ -4,7 +4,7 @@ applyTo: "**/*.tsx"
4
4
 
5
5
  # TSF++ React rules
6
6
 
7
- Full standard: `node_modules/@tsfpp/standard/REACT_CODING_STANDARD.md`
7
+ Full standard: `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md`
8
8
  Extends: tsfpp-base.instructions.md (all base rules apply to `.tsx` too)
9
9
 
10
10
  ## Component shape
@@ -1,152 +1,111 @@
1
1
  ---
2
- description: Scaffolds a new TSF++-compliant module with types, smart constructors, exports, JSDoc, and a test file skeleton.
3
- name: TSF++ new module
4
- argument-hint: "module=<name> layer=<core|api|dal|react> description=<one sentence>"
2
+ description: Targeted review of API boundary code against @tsfpp/boundary patterns and the API coding standard. Faster than a full audit — no report file, inline findings only.
3
+ name: TSF++ boundary review
4
+ argument-hint: "File(s) or directory to review, e.g. src/routes/tracks.ts"
5
5
  agent: agent
6
6
  tools:
7
- - edit/createFile
8
- - edit/editFiles
9
- - read/readFile
10
- - search/fileSearch
7
+ - read
8
+ - search/codebase
9
+ - search/textSearch
10
+ - search/usages
11
11
  - vscode/askQuestions
12
12
  ---
13
13
 
14
- # TSF++ new module
14
+ # TSF++ boundary review
15
15
 
16
- Scaffold a new TSF++-compliant module from scratch.
16
+ A focused, read-only review of API handler and route code against the `@tsfpp/boundary` patterns and the API coding standard.
17
17
 
18
- The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
19
- The prelude API is at `node_modules/@tsfpp/prelude/README.md`.
18
+ The API standard is at `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`.
19
+ The boundary API surface is at `node_modules/@tsfpp/boundary/README.md` and `node_modules/@tsfpp/boundary/RECIPES.md`.
20
+
21
+ > Read only. No file edits. Findings are reported inline in chat.
22
+ > For a full audit with a tracked report, use the `tsfpp-audit` agent with `focus: boundary`.
20
23
 
21
24
  ---
22
25
 
23
- ## Required inputs
26
+ ## Required input
24
27
 
25
- If any of the following are missing, ask for them before proceeding:
28
+ If a target has not been provided, ask:
26
29
 
27
- - **Module name** e.g. `track`, `artist`, `audio-asset`
28
- - **Layer** — `core` · `api` · `dal` · `react`
29
- - **Domain description** — one sentence: what does this module represent or do?
30
+ > Which file(s) or directory should I review? (e.g. `src/routes/tracks.ts`, `src/routes/`)
30
31
 
31
32
  ---
32
33
 
33
- ## What to generate
34
-
35
- ### 1. Source file — `src/<layer>/<module-name>.ts`
36
-
37
- ```ts
38
- /**
39
- * @module <module-name>
40
- *
41
- * <Domain description>.
42
- *
43
- * @packageDocumentation
44
- */
45
-
46
- import { type Option, type Result, some, none, ok, err } from '@tsfpp/prelude'
47
-
48
- // ─── Types ────────────────────────────────────────────────────────────────────
49
-
50
- /**
51
- * <What this branded type represents in the domain.>
52
- */
53
- export type <ModuleName>Id = Brand<string, '<ModuleName>Id'>
54
-
55
- /**
56
- * <What this sum type represents. List variants.>
57
- */
58
- export type <ModuleName> = {
59
- readonly id: <ModuleName>Id
60
- readonly <field>: <Type>
61
- // … additional fields
62
- }
63
-
64
- // ─── Errors ───────────────────────────────────────────────────────────────────
65
-
66
- /**
67
- * Errors that can occur when working with <ModuleName> values.
68
- */
69
- export type <ModuleName>Error =
70
- | { readonly kind: 'invalid_id'; readonly raw: string }
71
- | { readonly kind: 'not_found'; readonly id: <ModuleName>Id }
72
-
73
- // ─── Smart constructors ───────────────────────────────────────────────────────
74
-
75
- /**
76
- * Constructs a validated {@link <ModuleName>Id} from a raw string.
77
- *
78
- * @param raw - The raw string to validate.
79
- * @returns `some` with a branded id when valid; `none` when the format is invalid.
80
- *
81
- * @example
82
- * const id = mk<ModuleName>Id('abc-123')
83
- * // => some(<ModuleName>Id)
84
- */
85
- export const mk<ModuleName>Id = (raw: string): Option<<ModuleName>Id> =>
86
- raw.length > 0 ? some(raw as <ModuleName>Id) : none
87
-
88
- /**
89
- * Constructs a {@link <ModuleName>} from validated inputs.
90
- *
91
- * @param params - Validated field values.
92
- * @returns `ok` with the constructed value; `err` with a typed error on validation failure.
93
- */
94
- export const mk<ModuleName> = (params: {
95
- readonly id: <ModuleName>Id
96
- readonly <field>: <Type>
97
- }): Result<<ModuleName>, <ModuleName>Error> => {
98
- // validate invariants here
99
- return ok(params)
100
- }
101
- ```
34
+ ## Checklist
102
35
 
103
- ### 2. Test file `src/<layer>/<module-name>.test.ts`
104
-
105
- ```ts
106
- import { describe, expect, it } from 'vitest'
107
- import { isSome, isNone, isOk, isErr } from '@tsfpp/prelude'
108
- import { mk<ModuleName>Id, mk<ModuleName> } from './<module-name>'
109
-
110
- describe('mk<ModuleName>Id', () => {
111
- it('returns some for a valid id', () => {
112
- expect(isSome(mk<ModuleName>Id('abc-123'))).toBe(true)
113
- })
114
-
115
- it('returns none for an empty string', () => {
116
- expect(isNone(mk<ModuleName>Id(''))).toBe(true)
117
- })
118
- })
119
-
120
- describe('mk<ModuleName>', () => {
121
- it('returns ok for valid inputs', () => {
122
- // arrange
123
- // act
124
- // assert
125
- })
126
-
127
- it('returns err for invalid inputs', () => {
128
- // arrange
129
- // act
130
- // assert
131
- })
132
- })
133
- ```
36
+ Review every handler in the target against each item below. Report findings as a table at the end.
134
37
 
135
- ---
38
+ ### Handler shape
39
+
40
+ - [ ] Handler is a pure function: `(req: Request) => Promise<Response>`
41
+ - [ ] Only three steps: parse → call use-case → map response
42
+ - [ ] No business logic inside the handler body
43
+ - [ ] No database calls, logging setup, or infrastructure imports in the handler
44
+
45
+ ### Context extraction
46
+
47
+ - [ ] `extractContext` used to obtain `traceId` and `principalId`
48
+ - [ ] Raw headers (`req.headers.get(...)`) not accessed in business logic
49
+ - [ ] `traceId` passed to all `mkProblem` calls
50
+
51
+ ### Input validation
52
+
53
+ - [ ] All input validated with a Zod schema before entering the domain
54
+ - [ ] Schema defined adjacent to the route, not inline in the handler
55
+ - [ ] `fromZodError` used to map `ZodError` to a typed response
56
+ - [ ] No unvalidated `req.json()` or `req.body` passed to the domain
57
+
58
+ ### Response builders
59
+
60
+ - [ ] `okResponse` used for 200
61
+ - [ ] `createdResponse` used for 201
62
+ - [ ] `acceptedResponse` used for 202 (async operations)
63
+ - [ ] `noContentResponse` used for 204
64
+ - [ ] `problemResponse(mkProblem(...))` used for 4xx/5xx
65
+ - [ ] `new Response(...)` not constructed directly in a handler
66
+
67
+ ### Error mapping
136
68
 
137
- ## Rules
69
+ - [ ] `apiErrorToResponse` used as the single error mapping point
70
+ - [ ] No `throw` or `try/catch` in the handler body
71
+ - [ ] `fold` or `pipe` used to map `Result` to a response — no manual `if (isErr(...))` branching
138
72
 
139
- - Never use placeholder comments like `// TODO: implement` in the source file — either implement it or use a properly formatted `// TODO(unknown, <date>): <reason>` marker.
140
- - The error union must cover every failure mode the smart constructors can produce.
141
- - Every exported symbol must have a JSDoc block before the file is considered complete.
142
- - Test file must have at least one passing case and one failing case per smart constructor.
143
- - Follow layer-specific constraints from `tsfpp-guarded-coding` for the specified layer.
73
+ ### Security baseline
74
+
75
+ - [ ] Route requires authentication unless marked `// PUBLIC`
76
+ - [ ] `principalId` not logged at `info` level
77
+ - [ ] No user input reflected in error `detail` fields without sanitisation
78
+ - [ ] Mutating routes (`POST`, `PUT`, `PATCH`, `DELETE`) have idempotency handling or a documented reason why it is not needed
79
+
80
+ ### Imports
81
+
82
+ - [ ] All boundary primitives imported from `@tsfpp/boundary`
83
+ - [ ] No boundary primitives re-implemented locally
144
84
 
145
85
  ---
146
86
 
147
- ## Completion
87
+ ## Output format
88
+
89
+ Report findings as a table per handler:
90
+
91
+ ```
92
+ ## `POST /v1/tracks` — createTrackHandler
93
+
94
+ | Check | Status | Finding |
95
+ |-------|--------|---------|
96
+ | Handler shape | ✅ | — |
97
+ | Context extraction | ⚠️ | `req.headers.get('x-trace-id')` used directly on line 14 |
98
+ | Input validation | ✅ | — |
99
+ | Response builders | ❌ | `new Response(JSON.stringify(...), { status: 200 })` on line 31 |
100
+ | Error mapping | ✅ | — |
101
+ | Security baseline | ✅ | — |
102
+ | Imports | ✅ | — |
103
+ ```
104
+
105
+ After all handlers, append a one-line summary:
106
+
107
+ ```
108
+ 3 handlers reviewed · 2 findings · 1 clean
109
+ ```
148
110
 
149
- Report:
150
- 1. Files created and their paths
151
- 2. Exported symbols and their types
152
- 3. Any invariants that still need implementing (listed as `TODO` markers in the source)
111
+ If no findings are found, say so explicitly — do not omit the summary.
@@ -15,7 +15,7 @@ tools:
15
15
 
16
16
  Scaffold a new TSF++-compliant module from scratch.
17
17
 
18
- The canonical standard is at `node_modules/@tsfpp/standard/CODING_STANDARD.md`.
18
+ The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`.
19
19
  The prelude API is at `node_modules/@tsfpp/prelude/README.md`.
20
20
 
21
21
  ---
package/init.mjs CHANGED
@@ -2,19 +2,20 @@
2
2
  /**
3
3
  * @tsfpp/agents init
4
4
  *
5
- * Copies Copilot instructions, chatmodes, prompts, and CLAUDE.md
6
- * into the correct locations in the consumer's project.
5
+ * Copies Copilot agents, instructions, prompts, and Claude Code configuration
6
+ * into the correct locations in the consumer's project. Also generates
7
+ * eslint.config.js if not already present.
7
8
  *
8
9
  * Usage:
9
10
  * pnpm dlx @tsfpp/agents (one-shot, no install)
10
11
  * node node_modules/@tsfpp/agents/init.mjs
11
12
  */
12
13
 
13
- import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
14
- import { existsSync } from 'node:fs';
15
- import { join, dirname } from 'node:path';
16
- import { fileURLToPath } from 'node:url';
17
- import { createInterface } from 'node:readline';
14
+ import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
15
+ import { existsSync } from 'node:fs';
16
+ import { join, dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { createInterface } from 'node:readline';
18
19
 
19
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
21
  const cwd = process.cwd();
@@ -48,33 +49,149 @@ const FILES = [
48
49
  ['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
49
50
 
50
51
  // Claude Code
51
- ['claude/CLAUDE.md', 'CLAUDE.md'],
52
+ ['claude/CLAUDE.md', '.claude/CLAUDE.md'],
52
53
  ];
53
54
 
55
+ // ─── ESLint config generation ─────────────────────────────────────────────────
56
+
57
+ const PROFILES = {
58
+ base: { import: `import tsfpp from '@tsfpp/eslint-config'`, spread: 'tsfpp' },
59
+ react: { import: `import tsfppReact from '@tsfpp/eslint-config/react'`, spread: 'tsfppReact' },
60
+ api: { import: `import tsfppApi from '@tsfpp/eslint-config/api'`, spread: 'tsfppApi' },
61
+ };
62
+
63
+ async function detectWorkspacePackages() {
64
+ const wsFile = join(cwd, 'pnpm-workspace.yaml');
65
+ if (!existsSync(wsFile)) return null;
66
+
67
+ const yaml = await readFile(wsFile, 'utf8');
68
+ const patterns = [...yaml.matchAll(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/gm)]
69
+ .map(m => m[1].trim().replace(/\/\*\*?$/, '')); // strip trailing /* or /**
70
+
71
+ const packages = [];
72
+ for (const pattern of patterns) {
73
+ const absPattern = join(cwd, pattern);
74
+ if (!existsSync(absPattern)) continue;
75
+ const entries = await readdir(absPattern, { withFileTypes: true });
76
+ for (const entry of entries) {
77
+ if (entry.isDirectory()) packages.push(`${pattern}/${entry.name}`);
78
+ }
79
+ }
80
+ return packages.length > 0 ? packages : null;
81
+ }
82
+
83
+ async function askProfile(label) {
84
+ console.log(`\n Profile for ${bold(label)}:`);
85
+ console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
86
+ console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
87
+ console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
88
+ const choice = await ask(` ${dim('[1/2/3, default: 1]')} `);
89
+ return choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
90
+ }
91
+
92
+ function generateMonorepoConfig(packageProfiles) {
93
+ // Collect which profiles are used
94
+ const usedProfiles = [...new Set(Object.values(packageProfiles))];
95
+
96
+ const imports = usedProfiles.map(p => PROFILES[p].import).join('\n');
97
+
98
+ // base spreads globally (no files filter); react/api are scoped per package
99
+ const basePackages = Object.entries(packageProfiles)
100
+ .filter(([, p]) => p === 'base')
101
+ .map(([pkg]) => `'${pkg}/src/**'`);
102
+
103
+ const scopedProfiles = ['react', 'api'].flatMap(profile => {
104
+ const pkgs = Object.entries(packageProfiles)
105
+ .filter(([, p]) => p === profile)
106
+ .map(([pkg]) => `'${pkg}/src/**'`);
107
+ if (pkgs.length === 0) return [];
108
+ const spread = PROFILES[profile].spread;
109
+ return [
110
+ ` // ${profile}`,
111
+ ` ...${spread}.map(c => ({ ...c, files: [${pkgs.join(', ')}] })),`,
112
+ ];
113
+ });
114
+
115
+ const baseSpread = basePackages.length > 0
116
+ ? ` // base\n ...tsfpp.map(c => ({ ...c, files: [${basePackages.join(', ')}] })),`
117
+ : ` // base — applies to all files not matched by a scoped profile\n ...tsfpp,`;
118
+
119
+ return [
120
+ imports,
121
+ '',
122
+ 'export default [',
123
+ baseSpread,
124
+ ...scopedProfiles,
125
+ ']',
126
+ '',
127
+ ].join('\n');
128
+ }
129
+
130
+ function generateSingleConfig(profile) {
131
+ const { import: imp, spread } = PROFILES[profile];
132
+ return `${imp}\nexport default [...${spread}]\n`;
133
+ }
134
+
135
+ async function writeEslintConfig() {
136
+ const packages = await detectWorkspacePackages();
137
+
138
+ let content;
139
+ let description;
140
+
141
+ if (packages) {
142
+ console.log(`\n Monorepo detected — ${packages.length} package(s) found.\n`);
143
+ const packageProfiles = {};
144
+ for (const pkg of packages) {
145
+ packageProfiles[pkg] = await askProfile(pkg);
146
+ }
147
+ content = generateMonorepoConfig(packageProfiles);
148
+ description = 'monorepo';
149
+ } else {
150
+ const profile = await askProfile('this project');
151
+ content = generateSingleConfig(profile);
152
+ description = `profile: ${profile}`;
153
+ }
154
+
155
+ try {
156
+ await writeFile(join(cwd, 'eslint.config.js'), content, 'utf8');
157
+ results.copied.push('eslint.config.js');
158
+ console.log(`\n ${green('✓')} eslint.config.js ${dim(`(${description})`)}`);
159
+ } catch (err) {
160
+ results.failed.push('eslint.config.js');
161
+ console.log(`\n \x1b[31m✗\x1b[0m eslint.config.js ${dim(`(${err.message})`)}`);
162
+ }
163
+ }
164
+
54
165
  // ─── Helpers ─────────────────────────────────────────────────────────────────
55
166
 
56
167
  async function ensureDir(filePath) {
57
168
  await mkdir(dirname(filePath), { recursive: true });
58
169
  }
59
170
 
60
- async function confirm(question) {
171
+ async function ask(question) {
61
172
  const rl = createInterface({ input: process.stdin, output: process.stdout });
62
173
  return new Promise((resolve) => {
63
174
  rl.question(question, (answer) => {
64
175
  rl.close();
65
- resolve(answer.trim().toLowerCase() === 'y');
176
+ resolve(answer.trim().toLowerCase());
66
177
  });
67
178
  });
68
179
  }
69
180
 
181
+ async function confirm(question) {
182
+ return (await ask(question)) === 'y';
183
+ }
184
+
70
185
  // ─── Main ─────────────────────────────────────────────────────────────────────
71
186
 
72
187
  console.log();
73
188
  console.log(bold(' @tsfpp/agents — init'));
74
- console.log(dim(' Copies Copilot and Claude Code configuration into your project.\n'));
189
+ console.log(dim(' Sets up Copilot agents, instructions, and ESLint config.\n'));
75
190
 
76
191
  const results = { copied: [], skipped: [], failed: [] };
77
192
 
193
+ // ── Copy files ────────────────────────────────────────────────────────────────
194
+
78
195
  for (const [src, dest] of FILES) {
79
196
  const srcPath = join(__dirname, src);
80
197
  const destPath = join(cwd, dest);
@@ -101,7 +218,128 @@ for (const [src, dest] of FILES) {
101
218
  }
102
219
  }
103
220
 
104
- // ─── Summary ─────────────────────────────────────────────────────────────────
221
+ // ── Generate eslint.config.js ─────────────────────────────────────────────────
222
+
223
+ console.log();
224
+
225
+ const eslintDest = join(cwd, 'eslint.config.js');
226
+
227
+ if (existsSync(eslintDest)) {
228
+ const overwrite = await confirm(
229
+ ` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
230
+ );
231
+ if (!overwrite) {
232
+ results.skipped.push('eslint.config.js');
233
+ console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
234
+ } else {
235
+ await writeEslintConfig();
236
+ }
237
+ } else {
238
+ await writeEslintConfig();
239
+ }
240
+
241
+ async function writeEslintConfig() {
242
+ console.log(` Which ESLint profile does this project use?`);
243
+ console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
244
+ console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
245
+ console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
246
+
247
+ const choice = await ask(` ${dim('[1/2/3, default: 1]')} `);
248
+ const profile = choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
249
+
250
+ try {
251
+ await writeFile(eslintDest, ESLINT_PROFILES[profile], 'utf8');
252
+ results.copied.push('eslint.config.js');
253
+ console.log(` ${green('✓')} eslint.config.js ${dim(`(profile: ${profile})`)}`);
254
+ } catch (err) {
255
+ results.failed.push('eslint.config.js');
256
+ console.log(` \x1b[31m✗\x1b[0m eslint.config.js ${dim(`(${err.message})`)}`);
257
+ }
258
+ }
259
+
260
+ // ── Generate tsconfig.json ────────────────────────────────────────────────────
261
+
262
+ console.log();
263
+
264
+ await writeTsConfigs(await detectWorkspacePackages());
265
+
266
+ async function writeTsConfigs(packages) {
267
+ const PRESETS = {
268
+ app: { extends: '@tsfpp/tsconfig/app', label: 'app — application / tool (noEmit)' },
269
+ lib: { extends: '@tsfpp/tsconfig/lib', label: 'lib — publishable package (declaration, composite)' },
270
+ };
271
+
272
+ async function askPreset(label) {
273
+ console.log(`\n tsconfig preset for ${bold(label)}:`);
274
+ console.log(` ${dim('1')} app ${dim('— application / tool (noEmit: true)')}`);
275
+ console.log(` ${dim('2')} lib ${dim('— publishable package (declaration, composite)')}`);
276
+ const choice = await ask(` ${dim('[1/2, default: 1]')} `);
277
+ return choice === '2' ? 'lib' : 'app';
278
+ }
279
+
280
+ function generateTsConfig(preset, extra = {}) {
281
+ return JSON.stringify({
282
+ extends: PRESETS[preset].extends,
283
+ compilerOptions: { rootDir: 'src', ...extra },
284
+ include: ['src'],
285
+ }, null, 2) + '\n';
286
+ }
287
+
288
+ function generateRootTsConfig(packagePaths) {
289
+ return JSON.stringify({
290
+ files: [],
291
+ references: packagePaths.map(p => ({ path: p })),
292
+ }, null, 2) + '\n';
293
+ }
294
+
295
+ async function writeIfConfirmed(destPath, content, label) {
296
+ if (existsSync(destPath)) {
297
+ const overwrite = await confirm(
298
+ ` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
299
+ );
300
+ if (!overwrite) {
301
+ results.skipped.push(label);
302
+ console.log(` ${dim('–')} ${dim(label)} ${dim('(skipped)')}`);
303
+ return;
304
+ }
305
+ }
306
+ try {
307
+ await ensureDir(destPath);
308
+ await writeFile(destPath, content, 'utf8');
309
+ results.copied.push(label);
310
+ console.log(` ${green('✓')} ${label}`);
311
+ } catch (err) {
312
+ results.failed.push(label);
313
+ console.log(` \x1b[31m✗\x1b[0m ${label} ${dim(`(${err.message})`)}`);
314
+ }
315
+ }
316
+
317
+ if (packages) {
318
+ // Monorepo: tsconfig per package + root references
319
+ console.log(` Generating tsconfig.json per package.\n`);
320
+ const packagePresets = {};
321
+ for (const pkg of packages) {
322
+ packagePresets[pkg] = await askPreset(pkg);
323
+ }
324
+
325
+ for (const [pkg, preset] of Object.entries(packagePresets)) {
326
+ const destPath = join(cwd, pkg, 'tsconfig.json');
327
+ const content = generateTsConfig(preset);
328
+ await writeIfConfirmed(destPath, content, `${pkg}/tsconfig.json`);
329
+ }
330
+
331
+ // Root references tsconfig
332
+ const rootDest = join(cwd, 'tsconfig.json');
333
+ const rootContent = generateRootTsConfig(packages);
334
+ await writeIfConfirmed(rootDest, rootContent, 'tsconfig.json (root references)');
335
+ } else {
336
+ // Single package
337
+ const preset = await askPreset('this project');
338
+ const dest = join(cwd, 'tsconfig.json');
339
+ const content = generateTsConfig(preset);
340
+ await writeIfConfirmed(dest, content, 'tsconfig.json');
341
+ }
342
+ }
105
343
 
106
344
  console.log();
107
345
  console.log(dim(' ─────────────────────────────────────────'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsfpp/agents",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Workspace AI tooling for TSF++ projects: scoped instructions, coding agents, and reusable prompts",
5
5
  "keywords": [
6
6
  "tsfpp",