@luizsantiago/spec-guardrails 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/index.js +335 -0
- package/lib/archive.js +208 -0
- package/lib/assets.js +145 -0
- package/lib/brownfield.js +446 -0
- package/lib/config.js +293 -0
- package/lib/constants.js +262 -0
- package/lib/cursorrules.js +92 -0
- package/lib/delta-merge.js +248 -0
- package/lib/doctor.js +343 -0
- package/lib/download.js +133 -0
- package/lib/feature.js +272 -0
- package/lib/fs-utils.js +114 -0
- package/lib/gates.js +138 -0
- package/lib/install.js +140 -0
- package/lib/memory.js +34 -0
- package/lib/next-steps.js +50 -0
- package/lib/presets.js +176 -0
- package/lib/project-rules.js +210 -0
- package/lib/specs-utils.js +117 -0
- package/lib/token-cost.js +124 -0
- package/package.json +46 -0
- package/rules/engineering-baseline.mdc +56 -0
- package/scripts/_common.py +356 -0
- package/scripts/analyze_artifacts.py +187 -0
- package/scripts/check_commit.py +140 -0
- package/scripts/lessons.py +447 -0
- package/scripts/loop_plan.py +217 -0
- package/scripts/validate_spec.py +345 -0
- package/scripts/validate_state.py +385 -0
- package/scripts/validate_tasks.py +379 -0
- package/skills/agent-architecture.md +221 -0
- package/skills/appsec.md +83 -0
- package/skills/code-simplify.md +49 -0
- package/skills/engineering-standards.md +98 -0
- package/skills/git-handoff.md +213 -0
- package/skills/qa-strategy.md +83 -0
- package/skills/references/analyze.md +56 -0
- package/skills/references/archive.md +60 -0
- package/skills/references/constitution.md +66 -0
- package/skills/references/context-limits.md +73 -0
- package/skills/references/converge.md +47 -0
- package/skills/references/design.md +88 -0
- package/skills/references/discuss.md +68 -0
- package/skills/references/explore.md +61 -0
- package/skills/references/implement.md +175 -0
- package/skills/references/lessons.md +71 -0
- package/skills/references/memory.md +98 -0
- package/skills/references/project-init.md +62 -0
- package/skills/references/quick-mode.md +84 -0
- package/skills/references/specify.md +144 -0
- package/skills/references/sub-agents.md +117 -0
- package/skills/references/tasks.md +178 -0
- package/skills/references/validate.md +210 -0
- package/skills/security-review.md +120 -0
- package/skills/ship-ready.md +50 -0
- package/skills/task-graph-engineering.md +180 -0
- package/templates/GETTING_STARTED.md +61 -0
- package/templates/config.yaml.example +28 -0
- package/templates/presets/default.yaml +16 -0
- package/templates/presets/node-ts.yaml +22 -0
- package/templates/presets/python.yaml +22 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Granularity gate for `.specs/features/[feature]/tasks.md`.
|
|
3
|
+
|
|
4
|
+
Run before presenting a task breakdown for approval:
|
|
5
|
+
|
|
6
|
+
python3 validate_tasks.py .specs/features/auth/tasks.md
|
|
7
|
+
python3 validate_tasks.py auth
|
|
8
|
+
python3 validate_tasks.py # when the project has a single feature
|
|
9
|
+
|
|
10
|
+
Checks:
|
|
11
|
+
* at least one task with a well-formed ID (T1, T2, ...)
|
|
12
|
+
* every task carries Requirement, Files, Depends on, Tests, Gate and Done when
|
|
13
|
+
* Tests and Done when reject placeholder values (none, —, n/a)
|
|
14
|
+
* dependencies reference existing tasks, never forward, never self
|
|
15
|
+
* a task never depends on a task in a later `### Phase N` group
|
|
16
|
+
* dependency graph is acyclic
|
|
17
|
+
* every requirement ID in sibling spec.md is covered by at least one task
|
|
18
|
+
* independent tasks do not share Files paths
|
|
19
|
+
* 3+ tasks require sibling task-graph.md (when validating on disk)
|
|
20
|
+
* vague task titles are flagged as granularity smells
|
|
21
|
+
|
|
22
|
+
Exit codes: 0 pass, 1 blocking issues, 2 usage error.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from _common import (
|
|
33
|
+
Report,
|
|
34
|
+
find_placeholders,
|
|
35
|
+
normalize_file_path,
|
|
36
|
+
requirement_ids,
|
|
37
|
+
resolve_artifact,
|
|
38
|
+
visible_markdown,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
GATE = "validate-tasks"
|
|
42
|
+
|
|
43
|
+
TASK_HEADING = re.compile(
|
|
44
|
+
r"^#{2,6}\s*(?P<id>T\d{1,6})\s*[:\-–]?\s*(?P<title>.*)$",
|
|
45
|
+
re.MULTILINE | re.IGNORECASE,
|
|
46
|
+
)
|
|
47
|
+
FIELD = re.compile(
|
|
48
|
+
r"^\s*[-*]?\s*\*{0,2}(?P<key>[A-Za-z][A-Za-z ]+?)\*{0,2}\s*:\s*(?P<value>.+?)\s*$",
|
|
49
|
+
re.MULTILINE,
|
|
50
|
+
)
|
|
51
|
+
# Do not treat REQ-T100 as task T100: a hyphen glued to a letter is not a
|
|
52
|
+
# task-id boundary. Still matches T1, T12, and "see T3".
|
|
53
|
+
TASK_REF = re.compile(r"(?<![A-Za-z0-9-])T(\d{1,6})\b", re.IGNORECASE)
|
|
54
|
+
PHASE_HEADING = re.compile(
|
|
55
|
+
r"^#{1,6}\s*Phase\s+(?P<number>\d+)\b", re.MULTILINE | re.IGNORECASE
|
|
56
|
+
)
|
|
57
|
+
REQUIREMENT_REF = re.compile(r"\b[A-Z][A-Z0-9]{1,9}-\d{2,4}\b")
|
|
58
|
+
NONE_VALUES = {"-", "—", "–", "none", "n/a", "na", "nenhum", "nenhuma", "no"}
|
|
59
|
+
|
|
60
|
+
REQUIRED_FIELDS = ("requirement", "files", "depends on", "tests", "gate", "done when")
|
|
61
|
+
# Depends on: — remains valid (no deps). Files/Gate/Tests/Done when reject none/—.
|
|
62
|
+
PLACEHOLDER_FIELDS = frozenset({"done when", "tests", "gate", "files"})
|
|
63
|
+
VAGUE_TITLE_WORDS = {
|
|
64
|
+
"implement feature",
|
|
65
|
+
"create form",
|
|
66
|
+
"build ui",
|
|
67
|
+
"do backend",
|
|
68
|
+
"make it work",
|
|
69
|
+
"finish",
|
|
70
|
+
"misc",
|
|
71
|
+
"cleanup",
|
|
72
|
+
"refactor code",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_fields(body: str) -> dict[str, str]:
|
|
77
|
+
fields: dict[str, str] = {}
|
|
78
|
+
for match in FIELD.finditer(body):
|
|
79
|
+
key = match.group("key").strip().lower()
|
|
80
|
+
value = match.group("value").strip()
|
|
81
|
+
fields.setdefault(key, value)
|
|
82
|
+
return fields
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def split_tasks(text: str) -> list[tuple[str, str, str]]:
|
|
86
|
+
matches = list(TASK_HEADING.finditer(text))
|
|
87
|
+
tasks: list[tuple[str, str, str]] = []
|
|
88
|
+
|
|
89
|
+
for index, match in enumerate(matches):
|
|
90
|
+
start = match.end()
|
|
91
|
+
end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
|
|
92
|
+
tasks.append(
|
|
93
|
+
(match.group("id").upper(), match.group("title").strip(), text[start:end])
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return tasks
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def task_phases(text: str) -> dict[str, int]:
|
|
100
|
+
"""Map each task ID to the phase it sits under, or 0 when phases are unused."""
|
|
101
|
+
|
|
102
|
+
marks = [
|
|
103
|
+
(match.start(), int(match.group("number")))
|
|
104
|
+
for match in PHASE_HEADING.finditer(text)
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
if not marks:
|
|
108
|
+
return {}
|
|
109
|
+
|
|
110
|
+
phases: dict[str, int] = {}
|
|
111
|
+
|
|
112
|
+
for match in TASK_HEADING.finditer(text):
|
|
113
|
+
current = 0
|
|
114
|
+
for position, number in marks:
|
|
115
|
+
if position < match.start():
|
|
116
|
+
current = number
|
|
117
|
+
else:
|
|
118
|
+
break
|
|
119
|
+
phases[match.group("id").upper()] = current
|
|
120
|
+
|
|
121
|
+
return phases
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_dependencies(value: str) -> list[str]:
|
|
125
|
+
if not value or value.strip().lower() in NONE_VALUES:
|
|
126
|
+
return []
|
|
127
|
+
return [f"T{number}" for number in TASK_REF.findall(value)]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def parse_files(value: str) -> list[str]:
|
|
131
|
+
if not value or value.strip().lower() in NONE_VALUES:
|
|
132
|
+
return []
|
|
133
|
+
files: list[str] = []
|
|
134
|
+
for chunk in re.split(r"[,;\n]", value):
|
|
135
|
+
path = normalize_file_path(chunk)
|
|
136
|
+
if path and path.lower() not in NONE_VALUES:
|
|
137
|
+
files.append(path)
|
|
138
|
+
return files
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def detect_cycle(graph: dict[str, list[str]]) -> list[str] | None:
|
|
142
|
+
"""Iterative DFS so a long dependency chain cannot blow the call stack."""
|
|
143
|
+
|
|
144
|
+
visited: set[str] = set()
|
|
145
|
+
|
|
146
|
+
for root in graph:
|
|
147
|
+
if root in visited:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
path: list[str] = []
|
|
151
|
+
on_path: set[str] = set()
|
|
152
|
+
stack: list[tuple[str, bool]] = [(root, False)]
|
|
153
|
+
|
|
154
|
+
while stack:
|
|
155
|
+
node, expanded = stack.pop()
|
|
156
|
+
|
|
157
|
+
if expanded:
|
|
158
|
+
path.pop()
|
|
159
|
+
on_path.discard(node)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
if node in visited:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
if node in on_path:
|
|
166
|
+
return path[path.index(node):] + [node]
|
|
167
|
+
|
|
168
|
+
visited.add(node)
|
|
169
|
+
path.append(node)
|
|
170
|
+
on_path.add(node)
|
|
171
|
+
stack.append((node, True))
|
|
172
|
+
|
|
173
|
+
for neighbour in graph.get(node, []):
|
|
174
|
+
if neighbour in on_path:
|
|
175
|
+
return path[path.index(neighbour):] + [neighbour]
|
|
176
|
+
if neighbour not in visited:
|
|
177
|
+
stack.append((neighbour, False))
|
|
178
|
+
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def reachable(graph: dict[str, list[str]], start: str, goal: str) -> bool:
|
|
183
|
+
if start == goal:
|
|
184
|
+
return True
|
|
185
|
+
seen = {start}
|
|
186
|
+
stack = [start]
|
|
187
|
+
while stack:
|
|
188
|
+
node = stack.pop()
|
|
189
|
+
for neighbour in graph.get(node, []):
|
|
190
|
+
if neighbour == goal:
|
|
191
|
+
return True
|
|
192
|
+
if neighbour not in seen:
|
|
193
|
+
seen.add(neighbour)
|
|
194
|
+
stack.append(neighbour)
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_report(
|
|
199
|
+
target: str,
|
|
200
|
+
text: str,
|
|
201
|
+
*,
|
|
202
|
+
spec_text: str | None = None,
|
|
203
|
+
feature_dir: Path | None = None,
|
|
204
|
+
) -> Report:
|
|
205
|
+
report = Report(gate=GATE, target=target)
|
|
206
|
+
visible = visible_markdown(text)
|
|
207
|
+
tasks = split_tasks(visible)
|
|
208
|
+
|
|
209
|
+
if not tasks:
|
|
210
|
+
report.error("no tasks found - use '### T1: Short imperative title'")
|
|
211
|
+
return report
|
|
212
|
+
|
|
213
|
+
report.ok(f"{len(tasks)} task(s) with well-formed IDs")
|
|
214
|
+
|
|
215
|
+
order: dict[str, int] = {}
|
|
216
|
+
graph: dict[str, list[str]] = {}
|
|
217
|
+
seen: set[str] = set()
|
|
218
|
+
covered_requirements: set[str] = set()
|
|
219
|
+
files_by_task: dict[str, list[str]] = {}
|
|
220
|
+
|
|
221
|
+
for position, (task_id, title, body) in enumerate(tasks):
|
|
222
|
+
if task_id in seen:
|
|
223
|
+
report.error(f"duplicate task ID: {task_id}")
|
|
224
|
+
seen.add(task_id)
|
|
225
|
+
order[task_id] = position
|
|
226
|
+
|
|
227
|
+
if not title:
|
|
228
|
+
report.error(f"{task_id}: heading has no title")
|
|
229
|
+
elif title.strip().lower() in VAGUE_TITLE_WORDS:
|
|
230
|
+
report.error(f"{task_id}: title is not atomic: '{title}'")
|
|
231
|
+
elif len(title.split()) < 3:
|
|
232
|
+
report.warn(f"{task_id}: title may be too coarse: '{title}'")
|
|
233
|
+
|
|
234
|
+
fields = parse_fields(body)
|
|
235
|
+
|
|
236
|
+
for required in REQUIRED_FIELDS:
|
|
237
|
+
value = fields.get(required, "")
|
|
238
|
+
missing = not value
|
|
239
|
+
if required in PLACEHOLDER_FIELDS and value.strip().lower() in NONE_VALUES:
|
|
240
|
+
missing = True
|
|
241
|
+
if required == "files" and value and not parse_files(value):
|
|
242
|
+
missing = True
|
|
243
|
+
if missing:
|
|
244
|
+
report.error(f"{task_id}: missing '{required.title()}' field")
|
|
245
|
+
|
|
246
|
+
requirement = fields.get("requirement", "")
|
|
247
|
+
if requirement and not REQUIREMENT_REF.search(requirement):
|
|
248
|
+
report.error(
|
|
249
|
+
f"{task_id}: Requirement '{requirement}' does not reference a spec ID"
|
|
250
|
+
)
|
|
251
|
+
covered_requirements.update(REQUIREMENT_REF.findall(requirement))
|
|
252
|
+
|
|
253
|
+
files_by_task[task_id] = parse_files(fields.get("files", ""))
|
|
254
|
+
graph[task_id] = parse_dependencies(fields.get("depends on", ""))
|
|
255
|
+
|
|
256
|
+
phases = task_phases(visible)
|
|
257
|
+
if phases:
|
|
258
|
+
report.ok(f"{len(set(phases.values()))} execution phase(s) detected")
|
|
259
|
+
|
|
260
|
+
for task_id, dependencies in graph.items():
|
|
261
|
+
for dependency in dependencies:
|
|
262
|
+
if dependency == task_id:
|
|
263
|
+
report.error(f"{task_id}: depends on itself")
|
|
264
|
+
elif dependency not in seen:
|
|
265
|
+
report.error(f"{task_id}: depends on unknown task {dependency}")
|
|
266
|
+
elif order[dependency] > order[task_id]:
|
|
267
|
+
report.error(
|
|
268
|
+
f"{task_id}: forward dependency on {dependency} "
|
|
269
|
+
"- reorder tasks so dependencies come first"
|
|
270
|
+
)
|
|
271
|
+
elif phases.get(dependency, 0) > phases.get(task_id, 0):
|
|
272
|
+
report.error(
|
|
273
|
+
f"{task_id} (phase {phases.get(task_id, 0)}): depends on "
|
|
274
|
+
f"{dependency} from phase {phases[dependency]} "
|
|
275
|
+
"- a phase never depends on a later one"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
cycle = detect_cycle(graph)
|
|
279
|
+
if cycle:
|
|
280
|
+
report.error(f"dependency cycle detected: {' -> '.join(cycle)}")
|
|
281
|
+
else:
|
|
282
|
+
report.ok("dependency graph is acyclic")
|
|
283
|
+
|
|
284
|
+
independent = [task_id for task_id, deps in graph.items() if not deps]
|
|
285
|
+
if len(independent) > 1:
|
|
286
|
+
report.ok(
|
|
287
|
+
f"{len(independent)} task(s) without dependencies - candidates for parallel work "
|
|
288
|
+
"(see task-graph-engineering.md)"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
owners: dict[str, list[str]] = {}
|
|
292
|
+
for task_id, paths in files_by_task.items():
|
|
293
|
+
for path in paths:
|
|
294
|
+
owners.setdefault(path, []).append(task_id)
|
|
295
|
+
|
|
296
|
+
for path, writers in owners.items():
|
|
297
|
+
if len(writers) < 2:
|
|
298
|
+
continue
|
|
299
|
+
for index, left in enumerate(writers):
|
|
300
|
+
for right in writers[index + 1 :]:
|
|
301
|
+
if reachable(graph, left, right) or reachable(graph, right, left):
|
|
302
|
+
continue
|
|
303
|
+
report.error(
|
|
304
|
+
f"{left} and {right} both write '{path}' with no dependency "
|
|
305
|
+
"between them - merge the tasks or make one depend on the other"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if spec_text is not None:
|
|
309
|
+
spec_requirements = requirement_ids(visible_markdown(spec_text))
|
|
310
|
+
if spec_requirements:
|
|
311
|
+
missing = [
|
|
312
|
+
requirement_id
|
|
313
|
+
for requirement_id in spec_requirements
|
|
314
|
+
if requirement_id not in covered_requirements
|
|
315
|
+
]
|
|
316
|
+
if missing:
|
|
317
|
+
for requirement_id in missing:
|
|
318
|
+
report.error(
|
|
319
|
+
f"spec requirement {requirement_id} has no task "
|
|
320
|
+
"- map it in a task Requirement field"
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
report.ok(
|
|
324
|
+
f"all {len(spec_requirements)} spec requirement(s) covered by tasks"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
placeholders = find_placeholders(text)
|
|
328
|
+
if placeholders:
|
|
329
|
+
for item in placeholders[:10]:
|
|
330
|
+
report.error(f"unresolved placeholder at {item}")
|
|
331
|
+
else:
|
|
332
|
+
report.ok("no unresolved placeholders")
|
|
333
|
+
|
|
334
|
+
if len(seen) >= 3 and feature_dir is not None:
|
|
335
|
+
graph_path = feature_dir / "task-graph.md"
|
|
336
|
+
if graph_path.is_file():
|
|
337
|
+
report.ok("task-graph.md present for 3+ task breakdown")
|
|
338
|
+
else:
|
|
339
|
+
report.error(
|
|
340
|
+
"3+ tasks require task-graph.md - draw the DAG before approval "
|
|
341
|
+
"(see task-graph-engineering.md)"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return report
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _load_sibling_spec(tasks_path: Path) -> str | None:
|
|
348
|
+
spec_path = tasks_path.parent / "spec.md"
|
|
349
|
+
if spec_path.is_file():
|
|
350
|
+
return spec_path.read_text(encoding="utf-8")
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def main(argv: list[str] | None = None) -> int:
|
|
355
|
+
parser = argparse.ArgumentParser(description="Validate a feature tasks.md")
|
|
356
|
+
parser.add_argument(
|
|
357
|
+
"tasks",
|
|
358
|
+
nargs="?",
|
|
359
|
+
help="feature name, feature directory, or path to tasks.md",
|
|
360
|
+
)
|
|
361
|
+
parser.add_argument(
|
|
362
|
+
"--strict",
|
|
363
|
+
action="store_true",
|
|
364
|
+
help="treat warnings as blocking failures",
|
|
365
|
+
)
|
|
366
|
+
args = parser.parse_args(argv)
|
|
367
|
+
|
|
368
|
+
path, text = resolve_artifact(args.tasks, "tasks.md", GATE)
|
|
369
|
+
report = build_report(
|
|
370
|
+
str(path),
|
|
371
|
+
text,
|
|
372
|
+
spec_text=_load_sibling_spec(path),
|
|
373
|
+
feature_dir=path.parent,
|
|
374
|
+
)
|
|
375
|
+
return report.emit(strict=args.strict)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
if __name__ == "__main__":
|
|
379
|
+
sys.exit(main())
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-architecture
|
|
3
|
+
description: Spec-Driven Development hub for AI-assisted engineering. Progressive disclosure (~70% fewer skill tokens vs dumping the full kit). Adaptive phases with Python gates, independent verifier, discrimination sensor, evidence-or-zero, and .specs/ memory. Triggers on "specify feature", "design", "break into tasks", "implement", "verify", "quick fix", "resume work", "handoff".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent Architecture (Hub)
|
|
7
|
+
|
|
8
|
+
Spec-Driven Development (SDD) guardrails for AI-assisted software engineering.
|
|
9
|
+
Replaces "Vibe Coding" with adaptive phases backed by persistent memory, sister skills, and gates enforced by code.
|
|
10
|
+
|
|
11
|
+
**Token cost.** Load a working set, not the archive — see `references/context-limits.md`. Progressive phase loading is ~70% fewer skill tokens than dumping hub + all references + sister skills every turn; a Medium feature is typically ~80% cheaper in skill tokens than naive full reloads.
|
|
12
|
+
|
|
13
|
+
This file is the contract and the map. Phase procedures live in `references/`; cross-cutting concerns live in sister skills.
|
|
14
|
+
|
|
15
|
+
## Critical Rules (read before acting)
|
|
16
|
+
|
|
17
|
+
**Reference files.** Phase procedures live in `references/` next to this file (`.cursor/skills/references/`, `.claude/skills/references/`). Read a reference **completely** before acting on it. Never act on a partial read. Load the working set per `references/context-limits.md` — one feature at a time, current phase only.
|
|
18
|
+
|
|
19
|
+
**Gate scripts.** Structural gates live in `.specs/guardrails/scripts/` at the project root. Run them with `python3`; never assume a project-local `scripts/` directory belongs to Spec Guardrails.
|
|
20
|
+
|
|
21
|
+
**Execution contract — non-negotiable, holds even if no reference file is open:**
|
|
22
|
+
|
|
23
|
+
1. **Test-First Imperative** — Tests derive from the spec's acceptance criteria and assert spec-defined outcomes. They never mirror the implementation. No production code before spec and derived tests are approved.
|
|
24
|
+
2. **Gate before done** — A task is complete only when the project harness (tests, linter, compiler) passes. The runner decides, never self-assessment.
|
|
25
|
+
3. **One atomic commit per task** — Mark the task complete in `tasks.md` and include that update in the same commit. Never batch tasks; never weaken, skip, or delete tests to make them pass.
|
|
26
|
+
4. **Author ≠ verifier** — After the last task, `/verify` runs with a fresh, clean context that never wrote the code. It is mandatory, not prompted.
|
|
27
|
+
5. **Blast radius (git tiers)** — Approving a spec or tasks authorizes **Tier 0** local work only. Higher tiers need owner go-ahead.
|
|
28
|
+
|
|
29
|
+
| Tier | Authorized by spec/tasks approval | Owner go-ahead required |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| **0 — Local sandbox** | `feature-init`, feature folder, `git checkout -b feat/NNN-slug`, local commits (code + `.specs/`) | — |
|
|
32
|
+
| **1 — Share** | — | `git push`, open/update PR for this feature |
|
|
33
|
+
| **2 — External impact** | — | merge to default branch, deploy/release, force-push, production data or secrets |
|
|
34
|
+
|
|
35
|
+
Quick tier skips dedicated feature branches — commit on the current branch. See `git-handoff.md` for phase triggers.
|
|
36
|
+
|
|
37
|
+
## Deterministic Gates
|
|
38
|
+
|
|
39
|
+
Structural gates run **before** owner review, so they cannot drift when the model forgets a step.
|
|
40
|
+
|
|
41
|
+
| When | Command |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| Before `/specify` (Medium+) | `npx @luizsantiago/spec-guardrails feature-init "<description>"` (Tier 0) |
|
|
44
|
+
| Optional project config | `init-config --preset node-ts` or `install --preset python` (see `preset list`) |
|
|
45
|
+
| Before confirming a spec | `python3 .specs/guardrails/scripts/validate_spec.py [feature]` |
|
|
46
|
+
| Before approving tasks | `python3 .specs/guardrails/scripts/analyze_artifacts.py [feature]` |
|
|
47
|
+
| Before presenting tasks for approval | `python3 .specs/guardrails/scripts/validate_tasks.py [feature]` |
|
|
48
|
+
| On each commit | `python3 .specs/guardrails/scripts/check_commit.py --message "<message>"` |
|
|
49
|
+
| Before declaring a feature done | `python3 .specs/guardrails/scripts/validate_state.py [feature]` |
|
|
50
|
+
| After Verify PASS | `npx @luizsantiago/spec-guardrails archive-feature [feature]` (Tier 0) |
|
|
51
|
+
| Before a phase procedure (optional) | `npx @luizsantiago/spec-guardrails phase-context <phase>` |
|
|
52
|
+
| After a FAIL verdict | `python3 .specs/guardrails/scripts/lessons.py add --source .specs/features/[feature]/validation.md` |
|
|
53
|
+
|
|
54
|
+
Gates accept a feature name, a feature directory, or a path to the artifact. With no argument they auto-detect when the project has exactly one feature; with several they list candidates and exit 2. A spec is rejected unless every criterion uses `SHALL` or `MUST` and `## Assumptions` is present.
|
|
55
|
+
|
|
56
|
+
A **non-zero exit means STOP** — fix the artifact, then re-run the gate. Never continue past a failing gate.
|
|
57
|
+
|
|
58
|
+
**Degraded mode.** If Python 3 or shell execution is unavailable, say so once, then perform the same checks by reading the artifact against the reference checklist. Degraded mode never lowers the standard; it only changes who runs the check.
|
|
59
|
+
|
|
60
|
+
## Phase Map
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
EXPLORE (optional) → SPECIFY → DISCUSS (conditional) → DESIGN (optional) → TASKS (optional) → ANALYZE → EXECUTE (loop) → VERIFY → ARCHIVE
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Phase | Required | Reference | Sister skill | Gate |
|
|
67
|
+
| --- | --- | --- | --- | --- |
|
|
68
|
+
| **Explore** | Optional | `references/explore.md` | — | — |
|
|
69
|
+
| **Constitution** | Once per project | `references/constitution.md` | — | — |
|
|
70
|
+
| **Specify** | Yes | `references/specify.md` | — | `validate_spec.py` |
|
|
71
|
+
| **Discuss** | Conditional | `references/discuss.md` | — | — |
|
|
72
|
+
| **Design** | No | `references/design.md` | — | — |
|
|
73
|
+
| **Tasks** | No | `references/tasks.md` | `task-graph-engineering.md` | `validate_tasks.py` |
|
|
74
|
+
| **Analyze** | Before task approval | `references/analyze.md` | — | `analyze_artifacts.py` |
|
|
75
|
+
| **Execute** | Yes | `references/implement.md` | `engineering-standards.md` | `check_commit.py` |
|
|
76
|
+
| **Verify** | Yes | `references/validate.md` | `security-review.md` | `validate_state.py` |
|
|
77
|
+
| **Archive** | After Verify PASS | `references/archive.md` | `git-handoff.md` | `archive-feature` |
|
|
78
|
+
| **Converge** | On drift | `references/converge.md` | — | `analyze_artifacts.py` |
|
|
79
|
+
| **Handoff** | Yes | `references/memory.md` | `git-handoff.md` | — |
|
|
80
|
+
| **Quick** | Alternative | `references/quick-mode.md` | — | `check_commit.py` |
|
|
81
|
+
| **Context** | Always | `references/context-limits.md` | — | — |
|
|
82
|
+
| **Sub-agents** | When batched | `references/sub-agents.md` | `task-graph-engineering.md` | — |
|
|
83
|
+
| **Lessons** | On FAIL | `references/lessons.md` | — | `lessons.py` |
|
|
84
|
+
|
|
85
|
+
Context is a load rule, not a pipeline phase. Read it when the session is long or the feature has more than a handful of tasks. Sub-agents is the Execute scaling protocol — offer only when the task graph needs more than one batch; see `references/sub-agents.md`. Lessons is a FAIL-path step, not a sequential phase — see `references/lessons.md`.
|
|
86
|
+
|
|
87
|
+
## Conditional sister skills
|
|
88
|
+
|
|
89
|
+
Not in the default phase-map cell. Load only on `/verify` after `validate.md` + `security-review.md`, and **at most one in context at a time**.
|
|
90
|
+
|
|
91
|
+
| Skill | Load when | Skip when |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `appsec.md` | **Complex**, or auth / payments / PII / secrets / upload / SSRF / network trust boundary | Quick; Simple without those surfaces; copy/docs/styling |
|
|
94
|
+
| `qa-strategy.md` | **Complex**, or multi-step user-facing flow, or owner asked for regression/QA | Quick; Simple one-file; evidence-or-zero alone is enough |
|
|
95
|
+
|
|
96
|
+
**Sequence.** If both triggers fire: AppSec → write `## AppSec` → **drop** `appsec.md` from the working set → QA → write `## QA`. Never load both together. Neither section is enforced by `validate_state.py` (verifier judgment).
|
|
97
|
+
|
|
98
|
+
## Complexity Router
|
|
99
|
+
|
|
100
|
+
Complexity determines depth. Do not run every phase on every change.
|
|
101
|
+
|
|
102
|
+
| Tier | Scope | Path |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| **Quick** | ≤3 files, no design decisions, no new dependencies | `references/quick-mode.md` — describe, implement, verify, commit |
|
|
105
|
+
| **Simple** | 2–5 files, localized change | Specify → Execute → Verify |
|
|
106
|
+
| **Medium** | New feature, <10 tasks | Specify → Tasks → Execute → Verify |
|
|
107
|
+
| **Complex** | New architecture, API surface, infra | Specify → Discuss → Design → Tasks → Execute → Verify |
|
|
108
|
+
| **Parallel** | Splittable work, multiple agents | Above + `/task-graph` per `task-graph-engineering.md` |
|
|
109
|
+
|
|
110
|
+
**Hub Medium vs gate Medium+.** Router tiers above choose phase depth (hub **Medium** = new feature, under 10 tasks). The completion gate’s **Medium+** is separate: `design.md` with content, **or** 4+ tasks, **or** 2+ phases — that is when a discrimination-sensor outcome is blocking. A hub-Medium feature with only three tasks can be below gate Medium+.
|
|
111
|
+
|
|
112
|
+
**Rules**
|
|
113
|
+
|
|
114
|
+
- **Specify and Verify are always required on the full pipeline** — you must know WHAT was asked and prove it was delivered. **Quick** is the exception: the express lane in `references/quick-mode.md` (describe → implement → verify → commit) with only `check_commit.py` as a structural gate.
|
|
115
|
+
- **Design is skipped** when there are no architectural decisions and no new patterns.
|
|
116
|
+
- **Tasks is skipped** when there are ≤3 obvious steps.
|
|
117
|
+
- **Discuss is triggered inside Specify** when the feature touches persistence, external calls, auth, payments, concurrency, or state transitions, or when the owner's intent is ambiguous.
|
|
118
|
+
- **Safety valve** — Even when Tasks is skipped, Execute starts by listing atomic steps inline. If that listing reveals more than 5 steps or real dependencies, STOP and create a formal `tasks.md`; the Tasks phase was skipped in error.
|
|
119
|
+
|
|
120
|
+
When in doubt, start at **Medium** and drop phases only with owner approval.
|
|
121
|
+
|
|
122
|
+
## Persistent Memory (`.specs/`)
|
|
123
|
+
|
|
124
|
+
| Path | Purpose |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `.specs/STATE.md` | Decision log (`AD-NNN`) and handoff snapshot |
|
|
127
|
+
| `.specs/lessons.json` | Canonical lessons store, owned by `lessons.py` |
|
|
128
|
+
| `.specs/LESSONS.md` | Generated playbook of confirmed lessons — read, never write |
|
|
129
|
+
| `.specs/project/PROJECT.md` | Vision, stack, constraints (when the project defines them) |
|
|
130
|
+
| `.specs/project/CONSTITUTION.md` | Governing principles (when Constitution ran) |
|
|
131
|
+
| `.specs/project/ROADMAP.md` | Milestones and feature status |
|
|
132
|
+
| `.specs/config.yaml` | Optional project context and per-phase rules |
|
|
133
|
+
| `.specs/domains/[domain]/spec.md` | Long-lived domain truth after Archive |
|
|
134
|
+
| `.specs/quick/NNN-slug/` | Quick-mode tasks and summaries |
|
|
135
|
+
| `.specs/features/[feature]/spec.md` | Requirements (use `NNN-slug` from `feature-init`) |
|
|
136
|
+
| `.specs/features/[feature]/context.md` | Owner decisions for gray areas (only when Discuss ran) |
|
|
137
|
+
| `.specs/features/[feature]/design.md` | Architecture (Complex tier) |
|
|
138
|
+
| `.specs/features/[feature]/tasks.md` | Atomic task breakdown |
|
|
139
|
+
| `.specs/features/[feature]/task-graph.md` | Job DAG and parallel groups (when applicable) |
|
|
140
|
+
| `.specs/features/[feature]/validation.md` | Independent verification report |
|
|
141
|
+
| `.specs/guardrails/scripts/` | Deterministic gate scripts |
|
|
142
|
+
|
|
143
|
+
**Create artifacts lazily.** Write a file only when its phase actually produces content. Never scaffold an empty `design.md`, `tasks.md`, or `context.md` — an empty file claims a phase ran when it did not. Absence is the correct state for a skipped phase.
|
|
144
|
+
|
|
145
|
+
Read `STATE.md` at session start; update it at session end. See `references/memory.md` and `git-handoff.md`.
|
|
146
|
+
|
|
147
|
+
## Loop Engineering & Harness
|
|
148
|
+
|
|
149
|
+
- **Correction Loop** — If the project harness fails, fix and retest up to 3 times before escalating to the owner.
|
|
150
|
+
- **Operational Harness** — Quality is enforced by test runners, linters, and compilers, never by AI self-declaration.
|
|
151
|
+
- **Fix → re-verify** — Gaps found in Verify become fix tasks; the loop is bounded to 3 iterations before escalating.
|
|
152
|
+
|
|
153
|
+
## Knowledge Verification Chain
|
|
154
|
+
|
|
155
|
+
Follow in strict order when making any technical decision:
|
|
156
|
+
|
|
157
|
+
1. **Codebase** — Conventions and patterns already in use
|
|
158
|
+
2. **Project docs** — README, `docs/`, `.specs/STATE.md` decisions
|
|
159
|
+
3. **MCP / Context** — Up-to-date library documentation via tools
|
|
160
|
+
4. **Web search** — Official docs and community patterns
|
|
161
|
+
5. **Uncertainty** — Say "I don't know" and flag it. Never invent APIs or behaviors.
|
|
162
|
+
|
|
163
|
+
Never skip to step 5 while steps 1–4 are available. Fabrication cascades through design, tasks, and implementation.
|
|
164
|
+
|
|
165
|
+
## Output Behavior
|
|
166
|
+
|
|
167
|
+
- **Do the work; do not narrate the machinery.** Produce the artifact instead of announcing the phase.
|
|
168
|
+
- **Match effort to the work.** Heavy reasoning for design and ambiguity; fast execution for mechanical tasks.
|
|
169
|
+
- **Write artifacts in a plain, decided voice.** Lead with the verdict; cut filler and hedging.
|
|
170
|
+
- **Artifacts in English** — code, tests, commits, and `.specs/` documents (see `engineering-standards.md`). Chat language is the owner's personal setting, not guardrails rule.
|
|
171
|
+
|
|
172
|
+
## Model Selection
|
|
173
|
+
|
|
174
|
+
- **Planning** (Specify, Discuss, Design, Tasks): high-reasoning models
|
|
175
|
+
- **Execution loop**: fast, cost-effective models
|
|
176
|
+
- **Verifier**: mid-to-high tier — it performs adversarial reasoning and designs mutants
|
|
177
|
+
|
|
178
|
+
## Sister Skills
|
|
179
|
+
|
|
180
|
+
| Skill | Layer |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `task-graph-engineering.md` | Topology — task DAG, parallelism, diamond verify |
|
|
183
|
+
| `engineering-standards.md` | Quality — secure coding, one writer per file, artifact language |
|
|
184
|
+
| `security-review.md` | Verification — OWASP checklist for `/verify` |
|
|
185
|
+
| `appsec.md` | Conditional AppSec — threat sketch; Complex / attack surface only |
|
|
186
|
+
| `qa-strategy.md` | Conditional QA — smoke/regression; after AppSec if both apply |
|
|
187
|
+
| `code-simplify.md` | Conditional simplify — after A–D on Medium+ or owner ask; no behavior change |
|
|
188
|
+
| `ship-ready.md` | Conditional ship checklist — owner ask only; does not authorize push |
|
|
189
|
+
| `git-handoff.md` | Persistence — git sync, STATE template, session handoff |
|
|
190
|
+
|
|
191
|
+
Project rules: `.cursor/rules/engineering-baseline.mdc` (always applied in Cursor).
|
|
192
|
+
|
|
193
|
+
## Optional companion: Full Stack Floor Map
|
|
194
|
+
|
|
195
|
+
When [`@luizsantiago/fullstack-floor-map`](https://www.npmjs.com/package/@luizsantiago/fullstack-floor-map) is installed, **Execute** may load one **Lane** layer manual and at most one catalog specialist per turn; **`/verify` stays Guardrails-only** (no Lane manuals, no catalog). Floor Map **0.5.0 (planned)** adds **Desk** memory under `.specs/desks/` for specialist continuity and handoff — companion-owned, not Guardrails gates. Pairing contract: [Companion: Full Stack Floor Map](https://github.com/luizssantiago92/spec-guardrails/blob/main/docs/guide/Companion-fullstack-floor-map.md).
|
|
196
|
+
|
|
197
|
+
## Commands
|
|
198
|
+
|
|
199
|
+
| Command | Reference | Action |
|
|
200
|
+
| --- | --- | --- |
|
|
201
|
+
| `/explore` | `references/explore.md` | Think through ideas before Specify |
|
|
202
|
+
| `/project-init` | `references/project-init.md` | Brownfield: map repo → PROJECT + domain stubs |
|
|
203
|
+
| `/constitution` | `references/constitution.md` | Create project governing principles |
|
|
204
|
+
| `/specify` | `references/specify.md` | `feature-init` then requirements; EARS; delta specs |
|
|
205
|
+
| `/discuss` | `references/discuss.md` | Resolve gray areas into `context.md` |
|
|
206
|
+
| `/plan` | `references/design.md` | Create technical design |
|
|
207
|
+
| `/tasks` | `references/tasks.md` | Atomic breakdown; coverage matrix (authoring) |
|
|
208
|
+
| `/analyze` | `references/analyze.md` | Cross-artifact consistency before task approval |
|
|
209
|
+
| `/task-graph` | `task-graph-engineering.md` | Draw or revise the job DAG |
|
|
210
|
+
| `/loop` | `references/implement.md` | Orchestrate Execute — `loop-plan`, parallel sub-agents, adequacy A–D |
|
|
211
|
+
| `/verify` | `references/validate.md` | Independent validation; lean UAT; conditional AppSec/QA |
|
|
212
|
+
| `/converge` | `references/converge.md` | Reassess drift; append remaining tasks |
|
|
213
|
+
| `/archive` | `references/archive.md` | Fold verified feature into domain truth |
|
|
214
|
+
| `/quick` | `references/quick-mode.md` | Express lane for ≤3-file changes (no feature branch) |
|
|
215
|
+
| `/handoff` | `references/memory.md` | Update STATE, commit `.specs/` (Tier 0; no push) |
|
|
216
|
+
| `/sync-spec` | `git-handoff.md` | Commit current feature artifacts only |
|
|
217
|
+
| `/lessons` | `references/lessons.md` | Record or load grounded lessons |
|
|
218
|
+
|
|
219
|
+
## Credits
|
|
220
|
+
|
|
221
|
+
Lineage and inspiration (CC-BY / MIT notices): see the repository [Credits](https://github.com/luizssantiago92/spec-guardrails#credits) — TLC Spec-Driven, Addy Osmani agent-skills, graph-engineering.
|
package/skills/appsec.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# AppSec
|
|
2
|
+
|
|
3
|
+
Lean application-security pass for **Complex** work or features with a real attack surface. Complements `security-review.md` (OWASP checklist on `/verify`) — it does **not** replace that checklist.
|
|
4
|
+
|
|
5
|
+
**Judgment only.** `validate_state.py` does not require an `## AppSec` section. A skip with reason is valid. This skill does not make the product “secure”; it structures a short threat look and when to escalate to a human.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
Load during `/verify` **after** the base Verify steps (`validate.md` + `security-review.md`) when **any** of:
|
|
10
|
+
|
|
11
|
+
- Hub tier is **Complex**
|
|
12
|
+
- Feature touches auth, sessions, tokens, payments, PII, secrets, file upload, SSRF/URL fetch, or a network trust boundary
|
|
13
|
+
|
|
14
|
+
## When NOT to Use
|
|
15
|
+
|
|
16
|
+
Do **not** load on Quick, Simple without the surfaces above, copy/docs, or pure styling. Record:
|
|
17
|
+
|
|
18
|
+
`AppSec: skipped — no Complex tier and no auth/PII/payment/network surface`
|
|
19
|
+
|
|
20
|
+
Never load this skill in the same working set as `qa-strategy.md`. If both triggers fire: finish AppSec, **drop** this file from context, then load QA.
|
|
21
|
+
|
|
22
|
+
## Relationship to security-review
|
|
23
|
+
|
|
24
|
+
| Concern | Where |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| OWASP checklist / lightweight path | `security-review.md` (always on Verify) |
|
|
27
|
+
| Threat sketch, boundaries, escalate | **This skill** (conditional) |
|
|
28
|
+
|
|
29
|
+
Do not paste the OWASP list here. Reuse the Security Review result; deepen only the boundary and abuse cases.
|
|
30
|
+
|
|
31
|
+
## Procedure (about 15 minutes)
|
|
32
|
+
|
|
33
|
+
### 1. Threat sketch
|
|
34
|
+
|
|
35
|
+
Write briefly (in `validation.md` under `## AppSec`, or in `design.md` if Design already captured it):
|
|
36
|
+
|
|
37
|
+
- **Assets** — what must stay confidential or integral (tokens, PII, money, admin actions)
|
|
38
|
+
- **Actors** — anonymous, authenticated user, admin, external service
|
|
39
|
+
- **Trust boundaries** — browser ↔ API, API ↔ DB, API ↔ third party
|
|
40
|
+
- **Top 3 abuse cases** — concrete misuse (IDOR on resource X, token reuse, inject into field Y)
|
|
41
|
+
|
|
42
|
+
### 2. Focus list (pass / fail / N/A + one-line note)
|
|
43
|
+
|
|
44
|
+
Check only what the diff touches:
|
|
45
|
+
|
|
46
|
+
| Focus | Ask |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| AuthZ / IDOR | Can user A reach user B’s resource by changing an id? |
|
|
49
|
+
| Secrets | Credentials or keys only in env / secret store — not source, logs, or client storage? |
|
|
50
|
+
| Injection / XSS | User input parameterized or escaped on the paths this feature added? |
|
|
51
|
+
| Critical deps | New or bumped deps with known critical/high issues addressed or documented? |
|
|
52
|
+
| PII in logs | New log/response paths avoid raw PII? |
|
|
53
|
+
|
|
54
|
+
For checklist depth, return to `security-review.md`.
|
|
55
|
+
|
|
56
|
+
### 3. Escalate (stop and ask the owner)
|
|
57
|
+
|
|
58
|
+
Escalate instead of `Result: pass` when the feature introduces or materially changes:
|
|
59
|
+
|
|
60
|
+
- Payment capture or money movement
|
|
61
|
+
- Homegrown cryptography
|
|
62
|
+
- New multi-tenant isolation
|
|
63
|
+
- “We are not sure” on a trust boundary the owner must accept
|
|
64
|
+
|
|
65
|
+
## Output shape
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
## AppSec
|
|
69
|
+
- Applied: yes | skipped — [reason]
|
|
70
|
+
- Boundaries: [browser↔API / …]
|
|
71
|
+
- Top risks: [1], [2], [3]
|
|
72
|
+
- Focus: authZ … | secrets … | injection … | deps … | PII logs …
|
|
73
|
+
- Result: pass | fail | escalate
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`fail` or `escalate` → Gaps bullet + fix or owner decision before `Verdict: PASS` (verifier judgment; not a structural gate).
|
|
77
|
+
|
|
78
|
+
## Related
|
|
79
|
+
|
|
80
|
+
- `security-review.md` — OWASP Verify checklist
|
|
81
|
+
- `references/validate.md` — Verify procedure; load AppSec then drop before QA
|
|
82
|
+
- `references/context-limits.md` — at most one conditional sister in context
|
|
83
|
+
- [Gate stability](https://github.com/luizssantiago92/spec-guardrails/blob/main/prd/gate-stability.md) — AppSec is non-guarantee / judgment
|