@khanhspring/forge-spec 1.0.0
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/bin/install.js +33 -0
- package/forge-brainstorm/SKILL.md +332 -0
- package/forge-close/SKILL.md +129 -0
- package/forge-config/SKILL.md +113 -0
- package/forge-contract/SKILL.md +221 -0
- package/forge-contract/reference/best-practices.md +223 -0
- package/forge-contract/reference/contract-template.yaml +282 -0
- package/forge-init/SKILL.md +205 -0
- package/forge-spec/SKILL.md +279 -0
- package/forge-status/SKILL.md +55 -0
- package/forge-tasks/SKILL.md +79 -0
- package/package.json +22 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Main contract skeleton — copy and fill in for {feature-slug}.
|
|
2
|
+
# Placeholders in {curly braces} are substituted per feature/resource.
|
|
3
|
+
# Schemas and the shared/ error + pagination objects live in separate files (see best-practices.md);
|
|
4
|
+
# this main file holds only info + paths + $refs.
|
|
5
|
+
|
|
6
|
+
openapi: "3.0.0"
|
|
7
|
+
|
|
8
|
+
info:
|
|
9
|
+
title: "{Module Name} — {Feature Name}"
|
|
10
|
+
version: "1.0.0"
|
|
11
|
+
description: "Contract for {feature-slug}. Spec: ../../features/{slug}/spec.md"
|
|
12
|
+
|
|
13
|
+
servers:
|
|
14
|
+
- url: http://localhost:{port}
|
|
15
|
+
description: Local development
|
|
16
|
+
|
|
17
|
+
security:
|
|
18
|
+
- bearerAuth: []
|
|
19
|
+
|
|
20
|
+
paths:
|
|
21
|
+
/api/v1/{resources}:
|
|
22
|
+
post:
|
|
23
|
+
summary: "Create a new {resource}"
|
|
24
|
+
operationId: "create{Resource}"
|
|
25
|
+
tags: ["{feature-slug}"]
|
|
26
|
+
requestBody:
|
|
27
|
+
required: true
|
|
28
|
+
content:
|
|
29
|
+
application/json:
|
|
30
|
+
schema:
|
|
31
|
+
$ref: "./schemas/create-{resource}-request.yaml"
|
|
32
|
+
examples:
|
|
33
|
+
valid:
|
|
34
|
+
value:
|
|
35
|
+
{field}: "{example}"
|
|
36
|
+
responses:
|
|
37
|
+
"201":
|
|
38
|
+
description: "{Resource} created successfully"
|
|
39
|
+
headers:
|
|
40
|
+
Location:
|
|
41
|
+
description: "URL of the created resource"
|
|
42
|
+
schema:
|
|
43
|
+
type: string
|
|
44
|
+
example: "/api/v1/{resources}/123e4567-e89b-12d3-a456-426614174000"
|
|
45
|
+
content:
|
|
46
|
+
application/json:
|
|
47
|
+
schema:
|
|
48
|
+
$ref: "./schemas/{resource}-reference.yaml"
|
|
49
|
+
examples:
|
|
50
|
+
success:
|
|
51
|
+
value:
|
|
52
|
+
id: "123e4567-e89b-12d3-a456-426614174000"
|
|
53
|
+
"400":
|
|
54
|
+
$ref: "#/components/responses/BadRequest"
|
|
55
|
+
"401":
|
|
56
|
+
$ref: "#/components/responses/Unauthorized"
|
|
57
|
+
"409":
|
|
58
|
+
$ref: "#/components/responses/Conflict"
|
|
59
|
+
"500":
|
|
60
|
+
$ref: "#/components/responses/InternalError"
|
|
61
|
+
|
|
62
|
+
get:
|
|
63
|
+
summary: "List {resources}"
|
|
64
|
+
operationId: "list{Resources}"
|
|
65
|
+
tags: ["{feature-slug}"]
|
|
66
|
+
parameters:
|
|
67
|
+
- name: page
|
|
68
|
+
in: query
|
|
69
|
+
schema:
|
|
70
|
+
type: integer
|
|
71
|
+
default: 1
|
|
72
|
+
minimum: 1
|
|
73
|
+
- name: size
|
|
74
|
+
in: query
|
|
75
|
+
schema:
|
|
76
|
+
type: integer
|
|
77
|
+
default: 20
|
|
78
|
+
minimum: 1
|
|
79
|
+
maximum: 100
|
|
80
|
+
- name: sort
|
|
81
|
+
in: query
|
|
82
|
+
schema:
|
|
83
|
+
type: string
|
|
84
|
+
example: "createdAt"
|
|
85
|
+
- name: order
|
|
86
|
+
in: query
|
|
87
|
+
schema:
|
|
88
|
+
type: string
|
|
89
|
+
enum: [asc, desc]
|
|
90
|
+
default: desc
|
|
91
|
+
responses:
|
|
92
|
+
"200":
|
|
93
|
+
description: "Paginated list of {resources}"
|
|
94
|
+
content:
|
|
95
|
+
application/json:
|
|
96
|
+
schema:
|
|
97
|
+
type: object
|
|
98
|
+
required: [data, pagination]
|
|
99
|
+
properties:
|
|
100
|
+
data:
|
|
101
|
+
type: array
|
|
102
|
+
items:
|
|
103
|
+
$ref: "./schemas/{resource}-summary.yaml"
|
|
104
|
+
pagination:
|
|
105
|
+
$ref: "./shared/pagination-meta.yaml"
|
|
106
|
+
examples:
|
|
107
|
+
success:
|
|
108
|
+
value:
|
|
109
|
+
data:
|
|
110
|
+
- id: "123e4567-e89b-12d3-a456-426614174000"
|
|
111
|
+
{field}: "{value}"
|
|
112
|
+
pagination:
|
|
113
|
+
page: 1
|
|
114
|
+
size: 20
|
|
115
|
+
total: 1
|
|
116
|
+
totalPages: 1
|
|
117
|
+
"401":
|
|
118
|
+
$ref: "#/components/responses/Unauthorized"
|
|
119
|
+
"500":
|
|
120
|
+
$ref: "#/components/responses/InternalError"
|
|
121
|
+
|
|
122
|
+
/api/v1/{resources}/{id}:
|
|
123
|
+
parameters:
|
|
124
|
+
- name: id
|
|
125
|
+
in: path
|
|
126
|
+
required: true
|
|
127
|
+
schema:
|
|
128
|
+
type: string
|
|
129
|
+
format: uuid
|
|
130
|
+
|
|
131
|
+
get:
|
|
132
|
+
summary: "Get {resource} by ID"
|
|
133
|
+
operationId: "get{Resource}ById"
|
|
134
|
+
tags: ["{feature-slug}"]
|
|
135
|
+
responses:
|
|
136
|
+
"200":
|
|
137
|
+
description: "{Resource} found"
|
|
138
|
+
content:
|
|
139
|
+
application/json:
|
|
140
|
+
schema:
|
|
141
|
+
$ref: "./schemas/{resource}-response.yaml"
|
|
142
|
+
examples:
|
|
143
|
+
success:
|
|
144
|
+
value:
|
|
145
|
+
id: "123e4567-e89b-12d3-a456-426614174000"
|
|
146
|
+
{field}: "{value}"
|
|
147
|
+
createdAt: "2024-01-15T10:30:00Z"
|
|
148
|
+
"401":
|
|
149
|
+
$ref: "#/components/responses/Unauthorized"
|
|
150
|
+
"404":
|
|
151
|
+
$ref: "#/components/responses/NotFound"
|
|
152
|
+
"500":
|
|
153
|
+
$ref: "#/components/responses/InternalError"
|
|
154
|
+
|
|
155
|
+
patch:
|
|
156
|
+
summary: "Update {resource}"
|
|
157
|
+
operationId: "update{Resource}"
|
|
158
|
+
tags: ["{feature-slug}"]
|
|
159
|
+
requestBody:
|
|
160
|
+
required: true
|
|
161
|
+
content:
|
|
162
|
+
application/json:
|
|
163
|
+
schema:
|
|
164
|
+
$ref: "./schemas/update-{resource}-request.yaml"
|
|
165
|
+
examples:
|
|
166
|
+
valid:
|
|
167
|
+
value:
|
|
168
|
+
{field}: "{example}"
|
|
169
|
+
responses:
|
|
170
|
+
"204":
|
|
171
|
+
description: "Updated successfully"
|
|
172
|
+
"400":
|
|
173
|
+
$ref: "#/components/responses/BadRequest"
|
|
174
|
+
"401":
|
|
175
|
+
$ref: "#/components/responses/Unauthorized"
|
|
176
|
+
"403":
|
|
177
|
+
$ref: "#/components/responses/Forbidden"
|
|
178
|
+
"404":
|
|
179
|
+
$ref: "#/components/responses/NotFound"
|
|
180
|
+
"500":
|
|
181
|
+
$ref: "#/components/responses/InternalError"
|
|
182
|
+
|
|
183
|
+
delete:
|
|
184
|
+
summary: "Delete {resource}"
|
|
185
|
+
operationId: "delete{Resource}"
|
|
186
|
+
tags: ["{feature-slug}"]
|
|
187
|
+
responses:
|
|
188
|
+
"204":
|
|
189
|
+
description: "Deleted successfully"
|
|
190
|
+
"401":
|
|
191
|
+
$ref: "#/components/responses/Unauthorized"
|
|
192
|
+
"403":
|
|
193
|
+
$ref: "#/components/responses/Forbidden"
|
|
194
|
+
"404":
|
|
195
|
+
$ref: "#/components/responses/NotFound"
|
|
196
|
+
"500":
|
|
197
|
+
$ref: "#/components/responses/InternalError"
|
|
198
|
+
|
|
199
|
+
components:
|
|
200
|
+
responses:
|
|
201
|
+
BadRequest:
|
|
202
|
+
description: "Validation failed"
|
|
203
|
+
content:
|
|
204
|
+
application/json:
|
|
205
|
+
schema:
|
|
206
|
+
$ref: "./shared/api-error.yaml"
|
|
207
|
+
examples:
|
|
208
|
+
validationError:
|
|
209
|
+
value:
|
|
210
|
+
code: "VALIDATION_ERROR"
|
|
211
|
+
message: "Validation failed for 1 field(s)"
|
|
212
|
+
traceId: "123e4567-e89b-12d3-a456-426614174000"
|
|
213
|
+
details:
|
|
214
|
+
- field: "email"
|
|
215
|
+
code: "INVALID_FORMAT"
|
|
216
|
+
message: "Must be a valid email address"
|
|
217
|
+
Unauthorized:
|
|
218
|
+
description: "Not authenticated"
|
|
219
|
+
content:
|
|
220
|
+
application/json:
|
|
221
|
+
schema:
|
|
222
|
+
$ref: "./shared/api-error.yaml"
|
|
223
|
+
examples:
|
|
224
|
+
unauthenticated:
|
|
225
|
+
value:
|
|
226
|
+
code: "UNAUTHENTICATED"
|
|
227
|
+
message: "Missing or invalid authentication token"
|
|
228
|
+
traceId: "123e4567-e89b-12d3-a456-426614174000"
|
|
229
|
+
Forbidden:
|
|
230
|
+
description: "Not authorized"
|
|
231
|
+
content:
|
|
232
|
+
application/json:
|
|
233
|
+
schema:
|
|
234
|
+
$ref: "./shared/api-error.yaml"
|
|
235
|
+
examples:
|
|
236
|
+
forbidden:
|
|
237
|
+
value:
|
|
238
|
+
code: "FORBIDDEN"
|
|
239
|
+
message: "You do not have permission to perform this action"
|
|
240
|
+
traceId: "123e4567-e89b-12d3-a456-426614174000"
|
|
241
|
+
NotFound:
|
|
242
|
+
description: "Resource not found"
|
|
243
|
+
content:
|
|
244
|
+
application/json:
|
|
245
|
+
schema:
|
|
246
|
+
$ref: "./shared/api-error.yaml"
|
|
247
|
+
examples:
|
|
248
|
+
notFound:
|
|
249
|
+
value:
|
|
250
|
+
code: "NOT_FOUND"
|
|
251
|
+
message: "{Resource} not found"
|
|
252
|
+
traceId: "123e4567-e89b-12d3-a456-426614174000"
|
|
253
|
+
Conflict:
|
|
254
|
+
description: "Resource already exists or state conflict"
|
|
255
|
+
content:
|
|
256
|
+
application/json:
|
|
257
|
+
schema:
|
|
258
|
+
$ref: "./shared/api-error.yaml"
|
|
259
|
+
examples:
|
|
260
|
+
conflict:
|
|
261
|
+
value:
|
|
262
|
+
code: "ALREADY_EXISTS"
|
|
263
|
+
message: "{Resource} already exists"
|
|
264
|
+
traceId: "123e4567-e89b-12d3-a456-426614174000"
|
|
265
|
+
InternalError:
|
|
266
|
+
description: "Unexpected server error"
|
|
267
|
+
content:
|
|
268
|
+
application/json:
|
|
269
|
+
schema:
|
|
270
|
+
$ref: "./shared/api-error.yaml"
|
|
271
|
+
examples:
|
|
272
|
+
internalError:
|
|
273
|
+
value:
|
|
274
|
+
code: "INTERNAL_ERROR"
|
|
275
|
+
message: "An unexpected error occurred"
|
|
276
|
+
traceId: "123e4567-e89b-12d3-a456-426614174000"
|
|
277
|
+
|
|
278
|
+
securitySchemes:
|
|
279
|
+
bearerAuth:
|
|
280
|
+
type: http
|
|
281
|
+
scheme: bearer
|
|
282
|
+
bearerFormat: JWT
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "forge-init"
|
|
3
|
+
description: "One-time initialization for a Forge spec repo. Walks through setup conversationally one question at a time, then writes .forge/project.json, CLAUDE.md, and directory scaffolding on confirmation."
|
|
4
|
+
argument-hint: "Optional: project name to skip the first prompt"
|
|
5
|
+
compatibility: "Run once in a spec repo before any other forge skills. Safe to re-run — never overwrites existing files."
|
|
6
|
+
when_to_use: >
|
|
7
|
+
ALWAYS activate when any of the following occur:
|
|
8
|
+
- User says init, initialize, setup, set up, bootstrap, configure + repo/forge/project
|
|
9
|
+
- User says "set this up", "get this started", "prepare this repo"
|
|
10
|
+
- No .forge/project.json exists AND user wants to start planning features or writing specs
|
|
11
|
+
- User says "this is a new spec repo" or "add forge to this project"
|
|
12
|
+
Do NOT activate if .forge/project.json already exists — repo is already initialized.
|
|
13
|
+
metadata:
|
|
14
|
+
author: "forge-workflow"
|
|
15
|
+
source: "spec-skills/forge-init/SKILL.md"
|
|
16
|
+
user-invocable: true
|
|
17
|
+
disable-model-invocation: false
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Forge Init — Spec Repo
|
|
21
|
+
|
|
22
|
+
Initialize this repo as a Forge spec repo. Ask questions one at a time, build up the
|
|
23
|
+
config through conversation, then write everything in one go after confirmation.
|
|
24
|
+
|
|
25
|
+
## Pre-check
|
|
26
|
+
If `.forge/project.json` already exists → say "Spec repo already initialized." and stop.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Phase 1 — Project identity
|
|
31
|
+
|
|
32
|
+
Ask one question at a time. Wait for each answer before asking the next.
|
|
33
|
+
|
|
34
|
+
**Q1.** "What's the project name? (kebab-case slug, e.g. `my-platform`)"
|
|
35
|
+
|
|
36
|
+
**Q2.** "What does this project do? (1–2 sentences)"
|
|
37
|
+
|
|
38
|
+
**Q3.** "What's the spec repo remote URL?
|
|
39
|
+
_(Skip with 'none' if it's not set up yet — you can add it later with `/forge-config`)_"
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Phase 2 — Modules
|
|
44
|
+
|
|
45
|
+
Collect modules one at a time. Each module is gathered field by field.
|
|
46
|
+
|
|
47
|
+
Start with:
|
|
48
|
+
> "Now let's add the services and apps that make up this project. First module — what's its name? (kebab-case, e.g. `user-service`)"
|
|
49
|
+
|
|
50
|
+
For each module, ask these in order (one question per message):
|
|
51
|
+
|
|
52
|
+
1. **Name** — already asked above
|
|
53
|
+
2. **Type** — "Is `{name}` a backend service, frontend app, or something else?
|
|
54
|
+
_(backend / frontend / worker / gateway)_"
|
|
55
|
+
3. **Stack** — "What's the tech stack for `{name}`? (e.g. `Spring Boot 3, Java 21` or `React, TypeScript`)"
|
|
56
|
+
4. **Port** — "What port does `{name}` run on locally?"
|
|
57
|
+
5. **Repo URL** — "What's the git repo URL for `{name}`?
|
|
58
|
+
_(Skip with 'none' if not set up yet)_"
|
|
59
|
+
6. **Description** — "One sentence: what does `{name}` do?
|
|
60
|
+
_(Skip with 'none')_"
|
|
61
|
+
|
|
62
|
+
After collecting all fields for a module, confirm it back:
|
|
63
|
+
|
|
64
|
+
> "Got it:
|
|
65
|
+
> `{name}` — {type} — {stack} — :{port} — {repo or 'no repo yet'}
|
|
66
|
+
> {description}
|
|
67
|
+
> Is that right?"
|
|
68
|
+
|
|
69
|
+
Then ask:
|
|
70
|
+
> "Any more modules to add? (yes / no)"
|
|
71
|
+
|
|
72
|
+
If yes, repeat from step 1 of this phase for the next module.
|
|
73
|
+
If no, move to Phase 3.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Phase 3 — Principles & conventions
|
|
78
|
+
|
|
79
|
+
Ask one at a time. "None" or "not yet" are valid answers — these sections can be filled
|
|
80
|
+
in later with `/forge-config`.
|
|
81
|
+
|
|
82
|
+
**Q1.** "What are the core principles or rules for this project?
|
|
83
|
+
_(e.g. 'API contract before implementation', 'no breaking changes without versioning')
|
|
84
|
+
Say 'none yet' to skip._"
|
|
85
|
+
|
|
86
|
+
**Q2.** "Any coding or API conventions the whole team should follow?
|
|
87
|
+
_(e.g. 'all endpoints versioned under /api/v1', 'use kebab-case for URL paths')
|
|
88
|
+
Say 'none yet' to skip._"
|
|
89
|
+
|
|
90
|
+
**Q3.** "Anything explicitly out of scope or forbidden across services?
|
|
91
|
+
_(e.g. 'no direct DB access across service boundaries', 'no sync inter-service calls')
|
|
92
|
+
Say 'none yet' to skip._"
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Phase 4 — Preview & gate
|
|
97
|
+
|
|
98
|
+
Show a full preview of what will be written:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Ready to initialize. Here's what I'll create:
|
|
102
|
+
|
|
103
|
+
.forge/project.json
|
|
104
|
+
project: {name}
|
|
105
|
+
modules: {n} module(s): {comma-separated names}
|
|
106
|
+
spec_repo: {url or "not set"}
|
|
107
|
+
|
|
108
|
+
CLAUDE.md
|
|
109
|
+
Description: {description}
|
|
110
|
+
Modules: {n} listed
|
|
111
|
+
Principles: {list or "none yet"}
|
|
112
|
+
Conventions: {list or "none yet"}
|
|
113
|
+
Out of scope: {list or "none yet"}
|
|
114
|
+
|
|
115
|
+
features/.gitkeep
|
|
116
|
+
contracts/.gitkeep
|
|
117
|
+
.gitignore ← append Forge entries
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> "Does this look right? Say **yes** to initialize, or tell me what to change."
|
|
121
|
+
|
|
122
|
+
Wait for confirmation. Do not write anything before the user says yes.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Phase 5 — Write files
|
|
127
|
+
|
|
128
|
+
Write `.forge/project.json`:
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"project": "{project-name}",
|
|
132
|
+
"version": "1.0",
|
|
133
|
+
"modules": [
|
|
134
|
+
{
|
|
135
|
+
"name": "{module-name}",
|
|
136
|
+
"repo": "{repo-url}",
|
|
137
|
+
"type": "backend|frontend|worker|gateway",
|
|
138
|
+
"stack": ["{stack}"],
|
|
139
|
+
"port": {port},
|
|
140
|
+
"description": "{description}"
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
"spec_repo": "{spec-repo-url}",
|
|
144
|
+
"contract_format": "openapi3",
|
|
145
|
+
"contract_tool": "specmatic"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Write `CLAUDE.md`:
|
|
150
|
+
```markdown
|
|
151
|
+
# {project-name}
|
|
152
|
+
|
|
153
|
+
{description}
|
|
154
|
+
|
|
155
|
+
## Modules
|
|
156
|
+
| Name | Type | Stack | Port |
|
|
157
|
+
|------|------|-------|------|
|
|
158
|
+
| {name} | {type} | {stack} | {port} |
|
|
159
|
+
|
|
160
|
+
## Forge Workflow
|
|
161
|
+
1. `/forge-brainstorm` — explore and define a feature
|
|
162
|
+
2. `/forge-spec` — write the spec
|
|
163
|
+
3. `/forge-tasks` — break into per-module tasks
|
|
164
|
+
4. `/forge-contract` — generate Specmatic OpenAPI contracts
|
|
165
|
+
5. `/forge-close {slug} {module}` — mark a module's tasks done (after module repo ships)
|
|
166
|
+
|
|
167
|
+
Utilities: `/forge-status` (feature dashboard) · `/forge-config` (edit modules/ports)
|
|
168
|
+
|
|
169
|
+
## Principles
|
|
170
|
+
{list each as a bullet — or "None defined yet."}
|
|
171
|
+
|
|
172
|
+
## Conventions
|
|
173
|
+
{list each as a bullet — or "None defined yet."}
|
|
174
|
+
|
|
175
|
+
## Out of Scope / Forbidden
|
|
176
|
+
{list each as a bullet — or "None defined yet."}
|
|
177
|
+
|
|
178
|
+
## Structure
|
|
179
|
+
- `features/{slug}/brainstorm.md` — approved brainstorm summary
|
|
180
|
+
- `features/{slug}/spec.md` — requirements, flows, API list
|
|
181
|
+
- `features/{slug}/tasks.md` — per-module task breakdown with checkboxes
|
|
182
|
+
- `contracts/{module}/{slug}.yaml` + `schemas/` + `shared/` — Specmatic OpenAPI 3.0 contracts
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Create:
|
|
186
|
+
- `features/.gitkeep`
|
|
187
|
+
- `contracts/.gitkeep`
|
|
188
|
+
|
|
189
|
+
Append to `.gitignore` if not present:
|
|
190
|
+
```
|
|
191
|
+
# Forge
|
|
192
|
+
.forge/secrets
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Run `git status` and confirm:
|
|
196
|
+
> "Spec repo initialized. Run `/forge-brainstorm` to plan your first feature."
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Rules
|
|
201
|
+
- One question per message — never ask multiple questions at once
|
|
202
|
+
- Never write files before the user says yes in Phase 4
|
|
203
|
+
- Never overwrite existing files
|
|
204
|
+
- All written files must be complete — no unfilled placeholders
|
|
205
|
+
- If the user provides multiple answers in one message, accept them gracefully and move forward
|