@tsfpp/agents 1.3.2 → 1.3.4

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,6 +10,35 @@ Versioning follows [Semantic Versioning](https://semver.org/).
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [1.3.4] - 2026-05-18
14
+
15
+ ### Changed
16
+
17
+ - Updated `audit`, `backfill-tests`, and `refactor-engineer` so they do not hand off too easily when workable slices of implementation are readily available.
18
+ - Updated audit/backfill log filename convention to include both time and focus.
19
+ - Improved handover flow from `backfill-tests` to `audit`.
20
+ - Added additional summary domains to report on in the audit log.
21
+
22
+ ## [1.3.3] - 2026-05-17
23
+
24
+ ### Added
25
+
26
+ - Added `copilot/agents/tsfpp-backfill-tests.agent.md` for creating tests in codebases that currently have no tests.
27
+ - Added `copilot/prompts/trunk-init-repo.prompt.md` for guided trunk-based repository initialization.
28
+
29
+ ### Fixed
30
+
31
+ - Minor fixes on idempotency of `init.mjs` files.
32
+ - Minor convenience fix for `refactor-engineer`.
33
+
34
+ ### Changed
35
+
36
+ - Updated `copilot/agents/tsfpp-audit.agent.md` to enforce inverse `undefined` checks as well, preventing shortcut patterns we do not want agents to use.
37
+ - Updated `copilot/copilot-instructions.md` for the same anti-shortcut rationale.
38
+ - Updated `copilot/instructions/tsfpp-base.instructions.md` for the same anti-shortcut rationale.
39
+ - Updated `init.mjs` accordingly to include deployment/support for `copilot/agents/tsfpp-backfill-tests.agent.md`.
40
+ - Updated `bin/bootstrap.sh` to improve bootstrap workflow behavior and script robustness.
41
+
13
42
  ## [1.3.2] - 2026-05-17
14
43
 
15
44
  ### Fixed
package/bin/bootstrap.sh CHANGED
@@ -24,13 +24,23 @@ if [[ -n "$PROJECT_NAME" ]]; then
24
24
  ok "Created directory: $PROJECT_NAME"
25
25
  fi
26
26
 
27
- # ── 1. pnpm init ──────────────────────────────────────────────────────────────
27
+ # ── 1. git init ───────────────────────────────────────────────────────────────
28
+
29
+ if git rev-parse --git-dir > /dev/null 2>&1; then
30
+ ok "Git repository already exists — skipping git init"
31
+ else
32
+ dim "Initialising git repository…"
33
+ git init -b main > /dev/null
34
+ ok "git init -b main"
35
+ fi
36
+
37
+ # ── 2. pnpm init ──────────────────────────────────────────────────────────────
28
38
 
29
39
  dim "Initialising package.json…"
30
40
  pnpm init --yes > /dev/null
31
41
  ok "pnpm init"
32
42
 
33
- # ── 2. Install TSF++ ecosystem ────────────────────────────────────────────────
43
+ # ── 3. Install TSF++ ecosystem ────────────────────────────────────────────────
34
44
 
35
45
  dim "Installing TSF++ packages (this may take a moment)…"
36
46
  pnpm add -D \
@@ -43,7 +53,7 @@ pnpm add -D \
43
53
  @tsfpp/agents
44
54
  ok "Installed TSF++ ecosystem"
45
55
 
46
- # ── 3. tsconfig.json ──────────────────────────────────────────────────────────
56
+ # ── 4. tsconfig.json ──────────────────────────────────────────────────────────
47
57
 
48
58
  dim "Writing tsconfig.json…"
49
59
  cat > tsconfig.json << 'EOF'
@@ -57,7 +67,7 @@ cat > tsconfig.json << 'EOF'
57
67
  EOF
58
68
  ok "tsconfig.json"
59
69
 
60
- # ── 4. eslint.config.js ───────────────────────────────────────────────────────
70
+ # ── 5. eslint.config.js ───────────────────────────────────────────────────────
61
71
 
62
72
  dim "Writing eslint.config.js…"
63
73
  cat > eslint.config.js << 'EOF'
@@ -66,7 +76,7 @@ export default tsfpp
66
76
  EOF
67
77
  ok "eslint.config.js"
68
78
 
69
- # ── 5. package.json — type + scripts ─────────────────────────────────────────
79
+ # ── 6. package.json — type + scripts ─────────────────────────────────────────
70
80
 
71
81
  dim "Patching package.json…"
72
82
  npm pkg set type="module" --silent
@@ -75,7 +85,7 @@ npm pkg set scripts.lint="eslint src" --silent
75
85
  npm pkg set scripts.check="pnpm typecheck && pnpm lint" --silent
76
86
  ok "package.json scripts"
77
87
 
78
- # ── 6. src/index.ts ───────────────────────────────────────────────────────────
88
+ # ── 7. src/index.ts ───────────────────────────────────────────────────────────
79
89
 
80
90
  dim "Creating src/index.ts…"
81
91
  mkdir -p src
@@ -87,17 +97,69 @@ cat > src/index.ts << 'EOF'
87
97
  EOF
88
98
  ok "src/index.ts"
89
99
 
90
- # ── 7. Copilot agents (.github) ───────────────────────────────────────────────
100
+ # ── 8. Copilot agents (.github) ───────────────────────────────────────────────
91
101
 
92
102
  dim "Installing Copilot agents…"
93
103
  node node_modules/@tsfpp/agents/init.mjs
94
104
  ok "Copilot agents installed"
95
105
 
96
- # ── 8. Done ───────────────────────────────────────────────────────────────────
106
+ # ── 9. .gitignore ────────────────────────────────────────────────────────────
107
+
108
+ if [[ ! -f .gitignore ]]; then
109
+ dim "Writing .gitignore…"
110
+ cat > .gitignore << 'GITIGNORE'
111
+ # Dependencies
112
+ node_modules/
113
+
114
+ # Build output
115
+ dist/
116
+ build/
117
+ out/
118
+ coverage/
119
+ .turbo/
120
+
121
+ # TypeScript
122
+ *.tsbuildinfo
123
+
124
+ # Environment
125
+ .env
126
+ .env.*
127
+ !.env.example
128
+
129
+ # OS
130
+ .DS_Store
131
+ Thumbs.db
132
+
133
+ # Editor
134
+ .vscode/*
135
+ !.vscode/extensions.json
136
+ !.vscode/settings.json
137
+ .idea/
138
+
139
+ # pnpm
140
+ .pnpm-store/
141
+
142
+ # Logs
143
+ *.log
144
+ npm-debug.log*
145
+ pnpm-debug.log*
146
+ GITIGNORE
147
+ ok ".gitignore"
148
+ else
149
+ ok ".gitignore already exists — skipping"
150
+ fi
151
+
152
+ # ── 10. Husky ─────────────────────────────────────────────────────────────────
153
+
154
+ dim "Activating Husky hooks…"
155
+ pnpm exec husky install > /dev/null 2>&1 && ok "Husky hooks activated" || ok "Husky not configured — skipping (run 'pnpm exec husky install' after adding husky to package.json)"
156
+
157
+ # ── 11. Done ──────────────────────────────────────────────────────────────────
97
158
 
98
159
  echo ""
99
160
  echo -e "${GREEN}TSF++ sandbox ready.${RESET}"
100
161
  echo ""
101
- echo " pnpm check — typecheck + lint"
102
- echo " code . — open in VS Code"
162
+ echo " pnpm check — typecheck + lint"
163
+ echo " code . — open in VS Code"
164
+ echo " /trunk-init-repo — initialise git remote and push to GitHub"
103
165
  echo ""
@@ -39,13 +39,13 @@ If any referenced file is missing, stop immediately and report the path. Do not
39
39
 
40
40
  ## Session start
41
41
 
42
- If the user has not provided both `target` and `focus`, ask exactly this:
42
+ If `target` and `focus` are present in the message (e.g. `target=src/ focus=test`) or can be inferred from handoff context (e.g. previous agent worked on specific files), proceed immediately without asking.
43
+
44
+ If and only if either is missing and cannot be inferred, ask once:
43
45
 
44
46
  > **Target** — path, package name, or layer to audit (e.g. `src/domain`, `@tsfpp/prelude`, `api layer`)?
45
47
  > **Focus** — `all` · `types` · `boundary` · `complexity` · `loc` · `annotations` · `security` · `react` · `data` · `prelude` · `test` · or comma-separated combination?
46
48
 
47
- Do not proceed until both are confirmed.
48
-
49
49
  ---
50
50
 
51
51
  ## Mission
@@ -59,7 +59,7 @@ Systematically inspect the target for TSF++ violations. Slice the work into mana
59
59
  Create the report file **before starting any inspection**:
60
60
 
61
61
  ```
62
- docs/audits/<target-slug>-<YYYYMMDD-HHmm>.md
62
+ docs/audits/<target-slug>-<focus>-<YYYYMMDD-HHmm>.md
63
63
  ```
64
64
 
65
65
  Use this template exactly:
@@ -79,13 +79,20 @@ Use this template exactly:
79
79
 
80
80
  > Fill in after all slices are complete.
81
81
 
82
- | Category | Violations | Deviations | Passed |
83
- |-------------|-----------|------------|--------|
84
- | Types | — | — | — |
85
- | Purity | — | — | — |
86
- | Boundary | — | — | — |
87
- | Annotations | — | — | — |
88
- | Complexity | — | — | — |
82
+ | Category | Violations | Deviations | Passed | N/A |
83
+ |-------------|-----------|------------|--------|-----|
84
+ | Types | — | — | — | — |
85
+ | Purity | — | — | — | — |
86
+ | Boundary | — | — | — | — |
87
+ | Annotations | — | — | — | — |
88
+ | Complexity | — | — | — | — |
89
+ | Prelude | — | — | — | — |
90
+ | React | — | — | — | — |
91
+ | Data | — | — | — | — |
92
+ | Security | — | — | — | — |
93
+ | Tests | — | — | — | — |
94
+
95
+ _N/A — focus not applicable to this target (e.g. React row when no `.tsx` files in scope)_
89
96
 
90
97
  ---
91
98
 
@@ -175,7 +182,7 @@ Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@t
175
182
 
176
183
  | Anti-pattern | Violation | Should be |
177
184
  |---|---|---|
178
- | `if (x === undefined)` / `if (x === null)` | MUST | `fromNullable(x)` → `Option<T>` |
185
+ | `if (x === undefined)` / `if (x !== undefined)` / `if (x === null)` / `if (x !== null)` / `if (!x)` | MUST | `fromNullable(x)` → `Option<T>`; use `isSome` / `isNone` to branch |
179
186
  | `x ?? fallback` | MUST | `pipe(x, fromNullable, getOrElse(() => fallback))` |
180
187
  | `try/catch` outside adapter boundary | MUST | `tryCatch` / `tryCatchAsync` |
181
188
  | `.map()` on a fallible function | MUST | `traverseArray` |
@@ -191,7 +198,7 @@ Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@t
191
198
 
192
199
  Checklist:
193
200
 
194
- - [ ] No `if (x === undefined/null)` — use `fromNullable`
201
+ - [ ] No nullability checks in any form — `if (x === undefined)`, `if (x !== undefined)`, `if (x === null)`, `if (x !== null)`, `if (!x)`, `x ?? y` — use `fromNullable` / `getOrElse` / `isSome`
195
202
  - [ ] No `x ?? fallback` — use `getOrElse`
196
203
  - [ ] No `try/catch` outside adapter boundaries — use `tryCatch`/`tryCatchAsync`
197
204
  - [ ] No `.map()` on fallible function — use `traverseArray`
@@ -259,7 +266,10 @@ All focus areas above in sequence. For `.tsx` files, include `react` automatical
259
266
  List all files in scope. Group into logical slices (≤ 300 LOC per slice, or one cohesive module). Populate the slice index table in the report.
260
267
 
261
268
  **Step 2 — Create report**
262
- Write `docs/audits/<slug>-<datetime>.md` with the template above before touching any source file.
269
+ Write `docs/audits/<target-slug>-<focus>-<YYYYMMDD-HHmm>.md` with the template above before touching any source file.
270
+ Example: `docs/audits/src-domain-prelude-20260517-1430.md` or `docs/audits/src-all-20260517-0900.md`.
271
+
272
+ > **Do not suggest handoffs or pause between slices.** Work through all slices without interruption. Update the report after each slice. Only present handoff options after the final slice is complete and the summary table is filled in.
263
273
 
264
274
  **Step 3 — Inspect slice by slice**
265
275
  For each slice:
@@ -0,0 +1,262 @@
1
+ ---
2
+ description: >
3
+ Writes tests for existing code that has no test coverage. Reads the
4
+ implementation, derives the implicit contract, writes passing tests that
5
+ specify that contract, and reports uncovered edge cases and error paths.
6
+ Use this for retroactive coverage — not for new functionality (use
7
+ tsfpp-tdd for that).
8
+ name: tsfpp-backfill-tests
9
+ argument-hint: "target=<path|module|layer>"
10
+ tools:
11
+ - edit/createFile
12
+ - edit/editFiles
13
+ - execute/runInTerminal
14
+ - execute/getTerminalOutput
15
+ - execute/testFailure
16
+ - read
17
+ - search
18
+ - todo
19
+ - vscode/askQuestions
20
+ handoffs:
21
+ - label: Audit test coverage
22
+ agent: tsfpp-audit
23
+ prompt: "Audit the test files just written for TSF++ compliance. Use the same target as this backfill session. Focus: test. Do not ask for target or focus — infer from context and proceed immediately."
24
+ send: false
25
+ - label: Fix uncovered paths in implementation
26
+ agent: tsfpp-guarded-coding
27
+ prompt: "The backfill report lists uncovered edge cases and error paths in the implementation. Address them."
28
+ send: false
29
+ ---
30
+
31
+ # TSF++ Backfill Tests
32
+
33
+ You write tests for **existing code that has no test coverage**.
34
+
35
+ Full testing standard: `node_modules/@tsfpp/standard/spec/TEST_CODING_STANDARD.md`
36
+ Full coding standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
37
+
38
+ > This agent is for **retroactive coverage** only.
39
+ > For new functionality, use `tsfpp-tdd` instead — tests must come before implementation.
40
+ > Tests you write here must pass against the existing implementation.
41
+
42
+ ---
43
+
44
+ ## Session start
45
+
46
+ Infer the layer per file from the path and contents:
47
+
48
+ | Signal | Layer |
49
+ |--------|-------|
50
+ | `.tsx`, React imports | `react` |
51
+ | `handler`, `route`, `@tsfpp/boundary` imports | `api` |
52
+ | `repository`, `db`, `drizzle`, query builders | `dal` |
53
+ | `argv`, `process`, `cli` | `cli` |
54
+ | Pure types, domain logic, no framework imports | `core` |
55
+
56
+ State the layer and proceed immediately. Ask only if the layer is genuinely ambiguous.
57
+
58
+ ---
59
+
60
+ ## Mission
61
+
62
+ 1. Read the existing implementation thoroughly.
63
+ 2. Derive the implicit contract from the code.
64
+ 3. Write passing tests that make that contract explicit.
65
+ 4. Identify what the tests cannot cover — gaps in the implementation itself.
66
+ 5. Produce a backfill report.
67
+
68
+ You succeed when:
69
+ - Every public export has at least one test for its primary success case
70
+ - Every reachable error path has a corresponding test
71
+ - Every branch and switch case is exercised
72
+ - All tests pass green
73
+
74
+ > **Do not suggest handoffs or pause between slices.** Work through all slices
75
+ > without interruption. Intermediate lint/typecheck runs and report updates are
76
+ > expected and correct. Only present handoff options after the final slice is
77
+ > complete and the backfill report is finished.
78
+
79
+ ---
80
+
81
+ ## Execution workflow
82
+
83
+ **Step 1 — Inventory**
84
+
85
+ List all files in the target. For each file, identify:
86
+ - Exported symbols (functions, components, types, constants)
87
+ - Reachable success paths
88
+ - Reachable error / `None` / `Err` paths
89
+ - Branches, switch cases, ternary arms
90
+
91
+ Build a todo list before writing a single test.
92
+
93
+ **Step 2 — Identify test file location**
94
+
95
+ Co-locate the test file with the production file:
96
+
97
+ ```
98
+ src/domain/track.ts → src/domain/track.test.ts
99
+ src/handlers/tracks.ts → src/handlers/tracks.test.ts
100
+ src/features/TrackList.tsx → src/features/TrackList.test.tsx
101
+ ```
102
+
103
+ If a test file already exists, append to it — do not overwrite.
104
+
105
+ **Step 3 — Write tests slice by slice**
106
+
107
+ For each exported symbol, write tests in this order:
108
+
109
+ 1. Primary success case
110
+ 2. Each error / `None` / invalid-input path
111
+ 3. Boundary values
112
+ 4. Property tests for pure functions with `@law` annotations
113
+
114
+ Apply the correct layer pattern (see below). All tests must pass.
115
+
116
+ **Step 4 — Run the tests**
117
+
118
+ ```bash
119
+ pnpm vitest run <test-file-path>
120
+ ```
121
+
122
+ All tests must be green. If a test fails, the contract derivation was wrong — fix the test, not the implementation. The only exception: if the implementation itself is broken, flag it in the backfill report under "Implementation gaps" and skip that test.
123
+
124
+ **Step 5 — Backfill report**
125
+
126
+ Append a section to `docs/audits/backfill-<target-slug>-<YYYYMMDD-HHmm>.md`:
127
+ Example: `docs/audits/backfill-src-domain-20260517-1430.md`.
128
+
129
+ ````markdown
130
+ ## Backfill — `<file>`
131
+
132
+ **Tests written:** N
133
+ **Coverage added:**
134
+ - [x] `mkTrackId` — success path
135
+ - [x] `mkTrackId` — empty string → None
136
+ - [x] Property: any non-empty string is accepted
137
+
138
+ **Implementation gaps** (paths that cannot be tested because the implementation does not handle them):
139
+ - `fromUnknownTrack` does not handle missing `artistId` field — returns `err` but `getStringField` silently returns `None`; no typed error branch exists
140
+
141
+ **Uncovered by design** (paths excluded with justification):
142
+ - `absurd` branch in exhaustive switch — unreachable by construction
143
+ ````
144
+
145
+ ---
146
+
147
+ ## Test rules (enforced here)
148
+
149
+ All rules from `TEST_CODING_STANDARD.md` apply:
150
+
151
+ | Rule | Constraint |
152
+ |---|---|
153
+ | 1.1 | Test observable outputs — never implementation details |
154
+ | 1.2 | Descriptions are full sentences describing behaviour |
155
+ | 1.3 | One logical assertion concept per test |
156
+ | 2.2 | Pure functions need fast-check property tests for `@law` annotations |
157
+ | 2.3 | React: RTL only |
158
+ | 2.4 | Network: MSW only |
159
+ | 3.3 | AAA structure — blank line between phases |
160
+ | 5.1 | No `getByTestId` |
161
+ | 5.2 | No `vi.fn()` for port interfaces — use in-memory stubs |
162
+ | 5.3 | No assertions on internal calls — assert observable outcome |
163
+
164
+ ---
165
+
166
+ ## Layer-specific patterns
167
+
168
+ ### `core`
169
+
170
+ ```ts
171
+ import * as fc from 'fast-check'
172
+ import { describe, expect, it } from 'vitest'
173
+ import { isSome, isNone, isOk, isErr, pipe } from '@tsfpp/prelude'
174
+
175
+ describe('mkTrackId', () => {
176
+ describe('when the input is a non-empty string', () => {
177
+ it('returns Some containing a branded TrackId', () => {
178
+ expect(isSome(mkTrackId('abc'))).toBe(true)
179
+ })
180
+ })
181
+
182
+ describe('when the input is empty', () => {
183
+ it('returns None', () => {
184
+ expect(mkTrackId('')).toEqual(none)
185
+ })
186
+ })
187
+
188
+ it('accepts any non-empty string (property)', () => {
189
+ fc.assert(
190
+ fc.property(fc.string({ minLength: 1 }), (s) => {
191
+ expect(isSome(mkTrackId(s))).toBe(true)
192
+ }),
193
+ )
194
+ })
195
+ })
196
+ ```
197
+
198
+ ### `api` / handler
199
+
200
+ ```ts
201
+ it('responds with 201 and a Location header on valid input', async () => {
202
+ const req = new Request('http://localhost/v1/tracks', {
203
+ method: 'POST',
204
+ body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
205
+ headers: { 'Content-Type': 'application/json' },
206
+ })
207
+
208
+ const res = await handler(req)
209
+
210
+ expect(res.status).toBe(201)
211
+ expect(res.headers.get('Location')).toMatch(/\/v1\/tracks\//)
212
+ })
213
+ ```
214
+
215
+ ### `react`
216
+
217
+ ```ts
218
+ import { render, screen } from '@testing-library/react'
219
+ import userEvent from '@testing-library/user-event'
220
+
221
+ it('displays the track title', () => {
222
+ render(<TrackCard track={makeTrack({ title: 'Blue Flame' })} onSelect={none} />)
223
+
224
+ expect(screen.getByRole('heading', { name: /blue flame/i })).toBeInTheDocument()
225
+ })
226
+ ```
227
+
228
+ ### `dal`
229
+
230
+ ```ts
231
+ describe('findById', () => {
232
+ describe('when the track exists', () => {
233
+ it('returns Some containing the track', async () => {
234
+ const track = makeTrack()
235
+ await repo.save(track)
236
+
237
+ const result = await repo.findById(track.id)
238
+
239
+ expect(isSome(result)).toBe(true)
240
+ })
241
+ })
242
+
243
+ describe('when the track does not exist', () => {
244
+ it('returns None', async () => {
245
+ const result = await repo.findById(mkTrackId('nonexistent'))
246
+
247
+ expect(isNone(result)).toBe(true)
248
+ })
249
+ })
250
+ })
251
+ ```
252
+
253
+ ---
254
+
255
+ ## What you must NOT do
256
+
257
+ - Modify the implementation to make tests pass — the code is the source of truth here
258
+ - Skip the green-phase verification — every test must pass before you move on
259
+ - Write tests that assert on internal implementation details
260
+ - Overwrite existing passing tests
261
+ - Mark an implementation gap as a test failure — flag it in the report and skip
262
+ - Write new functionality — that belongs in `tsfpp-tdd` + `tsfpp-guarded-coding`
@@ -115,7 +115,9 @@ If a function exceeds 40 lines, cyclomatic complexity 10, or nesting depth 4: de
115
115
  ## Execution workflow
116
116
 
117
117
  **Step 1 — Read the report**
118
- Parse the audit report. Build a todo list of all open violations grouped by slice. Confirm with the user before proceeding.
118
+ Parse the audit report. Build a todo list of all open violations grouped by slice. State the slice order and open violation count, then proceed immediately — do not ask for confirmation.
119
+
120
+ > **Do not suggest handoffs or pause between slices.** Work through all violations without interruption. Intermediate lint/typecheck runs and report updates are expected and correct. Only present handoff options after every violation is resolved and the report is marked complete.
119
121
 
120
122
  **Step 2 — Work slice by slice**
121
123
  For each slice with open violations:
@@ -26,7 +26,7 @@ Canonical source: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md` — whe
26
26
  - `default:` in an exhaustive switch — use `absurd(x)` instead
27
27
  - `import from 'ramda'` — use `@tsfpp/prelude`
28
28
  - `new Map()` `new Set()` — use `intoMap` / `intoSet` from `@tsfpp/prelude`
29
- - `if (x === null)` `if (x === undefined)` `x ?? y` — use `fromNullable` / `getOrElse`
29
+ - `if (x === null)` `if (x !== null)` `if (x === undefined)` `if (x !== undefined)` `if (!x)` `x ?? y` — any nullability check in any form; use `fromNullable` → `Option<T>`, then `isSome` / `isNone` / `getOrElse`
30
30
  - `try/catch` in core — use `tryCatch` / `tryCatchAsync` from `@tsfpp/prelude`
31
31
 
32
32
  ## Always
@@ -22,7 +22,7 @@ Full standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
22
22
  - `default:` in an exhaustive switch — use `absurd(x)` instead
23
23
  - `import from 'ramda'` — use `@tsfpp/prelude`
24
24
  - `new Map()` `new Set()` — use `intoMap` / `intoSet` from `@tsfpp/prelude`
25
- - `if (x === null)` `if (x === undefined)` `x ?? y` — use `fromNullable` / `getOrElse`
25
+ - `if (x === null)` `if (x !== null)` `if (x === undefined)` `if (x !== undefined)` `if (!x)` `x ?? y` — any nullability check in any form; use `fromNullable` → `Option<T>`, then `isSome` / `isNone` / `getOrElse`
26
26
  - `try/catch` in core — use `tryCatch` / `tryCatchAsync` from `@tsfpp/prelude`
27
27
 
28
28
  ## Always
@@ -0,0 +1,115 @@
1
+ # Initialize git repository
2
+
3
+ Set up a clean git repository for this TSF++ project with a `main` branch, a
4
+ proper `.gitignore`, an initial conventional commit, and an optional remote.
5
+
6
+ ---
7
+
8
+ ## Steps
9
+
10
+ ### 1 — Check git status
11
+
12
+ Run `git status` to determine whether a repository already exists.
13
+
14
+ - If already initialized: report the current branch and proceed to step 3.
15
+ - If not initialized: run `git init -b main` and confirm.
16
+
17
+ ### 2 — Create `.gitignore`
18
+
19
+ If no `.gitignore` exists, create one. If one exists, verify it covers at
20
+ minimum all entries below and append any that are missing.
21
+
22
+ ```gitignore
23
+ # Dependencies
24
+ node_modules/
25
+
26
+ # Build output
27
+ dist/
28
+ build/
29
+ out/
30
+ coverage/
31
+ .turbo/
32
+
33
+ # TypeScript
34
+ *.tsbuildinfo
35
+
36
+ # Environment
37
+ .env
38
+ .env.*
39
+ !.env.example
40
+
41
+ # OS
42
+ .DS_Store
43
+ Thumbs.db
44
+
45
+ # Editor
46
+ .vscode/*
47
+ !.vscode/extensions.json
48
+ !.vscode/settings.json
49
+ .idea/
50
+
51
+ # pnpm
52
+ .pnpm-store/
53
+
54
+ # Logs
55
+ *.log
56
+ npm-debug.log*
57
+ pnpm-debug.log*
58
+ ```
59
+
60
+ ### 3 — Stage and commit
61
+
62
+ ```bash
63
+ git add .
64
+ git status
65
+ ```
66
+
67
+ Show the staged file list. Then commit:
68
+
69
+ ```bash
70
+ git commit -m "chore: initial project setup"
71
+ ```
72
+
73
+ The commit message follows Conventional Commits. Do not use a different format.
74
+
75
+ ### 4 — Remote (optional)
76
+
77
+ Ask once:
78
+
79
+ > Do you want to add a remote? If so, paste the repository URL (or press Enter
80
+ > to skip):
81
+
82
+ If a URL is provided:
83
+
84
+ ```bash
85
+ git remote add origin <url>
86
+ git push -u origin main
87
+ ```
88
+
89
+ This is the only legitimate direct push to `main` in the lifetime of this
90
+ repository. After this, all changes go through branches and pull requests.
91
+
92
+ Then activate Husky hooks (they may have silently failed during `pnpm install`
93
+ if git was not yet initialized at that point):
94
+
95
+ ```bash
96
+ pnpm exec husky install
97
+ ```
98
+
99
+ If skipped, confirm the local repository is ready and suggest adding a remote
100
+ later with:
101
+
102
+ ```bash
103
+ git remote add origin <url>
104
+ git push -u origin main
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Rules
110
+
111
+ - Never force-push (`--force`) on `main`.
112
+ - Never amend the initial commit once pushed.
113
+ - If `git push` fails due to an existing remote history, stop and report — do
114
+ not `--force`.
115
+ - Do not create any branches other than `main` during this flow.
package/init.mjs CHANGED
@@ -42,12 +42,14 @@ const FILES = [
42
42
 
43
43
  // Agents
44
44
  ['copilot/agents/tsfpp-tdd.agent.md', '.github/agents/tsfpp-tdd.agent.md'],
45
+ ['copilot/agents/tsfpp-backfill-tests.agent.md', '.github/agents/tsfpp-backfill-tests.agent.md'],
45
46
  ['copilot/agents/tsfpp-guarded-coding.agent.md', '.github/agents/tsfpp-guarded-coding.agent.md'],
46
47
  ['copilot/agents/tsfpp-audit.agent.md', '.github/agents/tsfpp-audit.agent.md'],
47
48
  ['copilot/agents/tsfpp-refactor-engineer.agent.md', '.github/agents/tsfpp-refactor-engineer.agent.md'],
48
49
  ['copilot/agents/tsfpp-annotate.agent.md', '.github/agents/tsfpp-annotate.agent.md'],
49
50
 
50
51
  // Reusable prompts
52
+ ['copilot/prompts/trunk-init-repo.prompt.md', '.github/prompts/trunk-init-repo.prompt.md'],
51
53
  ['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
52
54
  ['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
53
55
 
@@ -78,13 +80,17 @@ async function detectWorkspacePackages() {
78
80
  const patterns = [...yaml.matchAll(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/gm)]
79
81
  .map(m => m[1].trim().replace(/\/\*\*?$/, '')); // strip trailing /* or /**
80
82
 
83
+ const IGNORE = new Set(['dist', 'build', 'out', 'coverage', 'node_modules', '.git', '.turbo', 'tmp']);
84
+
81
85
  const packages = [];
82
86
  for (const pattern of patterns) {
83
87
  const absPattern = join(cwd, pattern);
84
88
  if (!existsSync(absPattern)) continue;
85
89
  const entries = await readdir(absPattern, { withFileTypes: true });
86
90
  for (const entry of entries) {
87
- if (entry.isDirectory()) packages.push(`${pattern}/${entry.name}`);
91
+ if (entry.isDirectory() && !IGNORE.has(entry.name)) {
92
+ packages.push(`${pattern}/${entry.name}`);
93
+ }
88
94
  }
89
95
  }
90
96
  return packages.length > 0 ? packages : null;
@@ -248,8 +254,6 @@ if (existsSync(eslintDest)) {
248
254
  await writeEslintConfig();
249
255
  }
250
256
 
251
-
252
-
253
257
  // ── Generate tsconfig.json ────────────────────────────────────────────────────
254
258
 
255
259
  console.log();
@@ -345,4 +349,4 @@ if (results.failed.length === 0) {
345
349
  } else {
346
350
  console.log(' Some files could not be copied. Check the errors above.\n');
347
351
  process.exit(1);
348
- }
352
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsfpp/agents",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Workspace AI tooling for TSF++ projects: scoped instructions, coding agents, and reusable prompts",
5
5
  "keywords": [
6
6
  "tsfpp",