@luizsantiago/spec-guardrails 4.0.0 → 4.1.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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Spec Guardrails is a process kit that installs phase guides, persistent project memory, and optional structural checks into your repository. Teams keep ownership of requirements and approval gates; agents follow a repeatable path from written intent to verified delivery.
9
9
 
10
- npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.0.x**
10
+ npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.1.x**
11
11
 
12
12
  ---
13
13
 
@@ -94,10 +94,11 @@ Enable when the work warrants them — most teams start with the core loop only.
94
94
  | Episodic memory | Session notes → lessons for future runs |
95
95
  | Code index | Lightweight brownfield file and symbol map |
96
96
  | Solution exploration | Compare implementation options before committing |
97
- | Sandbox / execution policy | Warn or block destructive shell commands and path drift |
97
+ | Sandbox policy | Warn or block destructive shell commands (Cursor hook) |
98
+ | Execution policy | Path allowlists, budgets, read/write/delete effects |
98
99
  | Semantic retrieval | Search by meaning — off by default |
99
100
 
100
- Guides: [Memory](docs/guide/Memory.md) · [Brownfield context](docs/guide/brownfield-context.md) · [Safety & exploration](docs/guide/Overview.md)
101
+ Guides: [Memory](docs/guide/Memory.md) · [Cursor hooks and sandbox](docs/guide/Cursor-hooks-and-sandbox.md) · [Brownfield context](docs/guide/brownfield-context.md)
101
102
 
102
103
  ---
103
104
 
@@ -123,6 +124,7 @@ Start with the guide that matches your question; each page links deeper where ne
123
124
  | --- | --- | --- |
124
125
  | Orientation | [Overview](docs/guide/Overview.md) | [Concepts](docs/guide/concepts.md) |
125
126
  | First session | [Quick start](docs/guide/Quick-start.md) | [Agent commands](docs/guide/agent-commands.md) |
127
+ | Cursor IDE protection | [Cursor hooks and sandbox](docs/guide/Cursor-hooks-and-sandbox.md) | [Guarantees matrix](docs/guide/Guarantees-matrix.md) |
126
128
  | Process model | [How it works](docs/guide/How-it-works.md) | [Loop patterns](docs/guide/loop-patterns.md) |
127
129
  | Enforcement | [Gates](docs/guide/gates.md) | [Gates and guarantees](docs/guide/Gates-and-guarantees.md) |
128
130
  | Long-running projects | [Memory](docs/guide/Memory.md) | [Brownfield context](docs/guide/brownfield-context.md) |
package/index.js CHANGED
@@ -87,6 +87,11 @@ Commands:
87
87
  req-analysis discover List local kickoff sources (prd.md, kickoff.md, …)
88
88
  req-analysis promote Print next steps after brief approval
89
89
  [--scope project|feature] Match the brief scope
90
+ req-analysis validate [brief.md] Gate: approved requirements brief before /specify
91
+ req-analysis context Assemble kickoff + brief context for Specify
92
+ [--scope project|feature] Scope (default: project)
93
+ [--slug <feature-slug>] Feature slug when scope=feature
94
+ [--json] Machine-readable output
90
95
  archive-feature [feature] Fold verified feature into ROADMAP + domain spec; reset STATE
91
96
  [--domain <slug>] Domain folder under .specs/domains/ (default: feature slug)
92
97
  [--skip-verify] Skip validate-state (tests / recovery only)
@@ -168,6 +173,7 @@ Commands:
168
173
  [--json] Machine-readable plan for agents
169
174
  validate-traceability [feature] REQ → tasks → validation coverage chain
170
175
  validate-quick [quick-folder] Quick-mode TASK.md / SUMMARY.md structural gate
176
+ validate-req-analysis [brief.md] Requirements brief gate before /specify (/elicit)
171
177
  validate-state [feature] Completion gate before declaring a feature done
172
178
  check-commit --message "<msg>" Conventional Commits gate
173
179
  lessons <add|list|penalize|prune|promote|graduate|status> Lessons engine
@@ -953,8 +959,41 @@ if (command === "--version" || command === "-v" || command === "version") {
953
959
  }
954
960
  }
955
961
  console.log(formatPromoteMessage({ scope, description }));
962
+ } else if (sub === "validate") {
963
+ const briefPath = rest.find((arg) => !arg.startsWith("--"));
964
+ const code = await runGate("validate-req-analysis", briefPath ? [briefPath] : []);
965
+ process.exit(code);
966
+ } else if (sub === "context") {
967
+ let scope = "project";
968
+ let slug = "";
969
+ let json = false;
970
+ for (let i = 0; i < rest.length; i += 1) {
971
+ const arg = rest[i];
972
+ if (arg === "--json") {
973
+ json = true;
974
+ } else if (arg === "--scope" && rest[i + 1]) {
975
+ scope = rest[i + 1];
976
+ i += 1;
977
+ } else if (arg.startsWith("--scope=")) {
978
+ scope = arg.slice("--scope=".length);
979
+ } else if (arg === "--slug" && rest[i + 1]) {
980
+ slug = rest[i + 1];
981
+ i += 1;
982
+ } else if (arg.startsWith("--slug=")) {
983
+ slug = arg.slice("--slug=".length);
984
+ }
985
+ }
986
+ const scriptArgs = ["--scope", scope];
987
+ if (slug) {
988
+ scriptArgs.push("--slug", slug);
989
+ }
990
+ if (json) {
991
+ scriptArgs.push("--json");
992
+ }
993
+ const code = await runGuardrailsScript("req-context", scriptArgs);
994
+ process.exit(code);
956
995
  } else {
957
- throw new Error("Usage: req-analysis init | discover | promote");
996
+ throw new Error("Usage: req-analysis init | discover | promote | validate | context");
958
997
  }
959
998
  } catch (err) {
960
999
  console.error(`❌ ${err.message}`);
@@ -26,12 +26,21 @@ const DEPENDENCY_SIGNALS = [
26
26
  /\badd\s+package\b/i,
27
27
  ];
28
28
 
29
+ const VAGUE_SIGNALS = [
30
+ /\bimprove\b/i,
31
+ /\bmake\s+(?:it|this|things?)\s+better\b/i,
32
+ /\badd\s+(?:a\s+)?(?:interface|page|screen|ui|dashboard)\b/i,
33
+ /\b(?:somehow|something|stuff)\b/i,
34
+ /\bwithout\s+(?:criteria|details|spec)\b/i,
35
+ ];
36
+
29
37
  /**
30
38
  * @param {{ description?: string, files?: string[] }} input
31
39
  * @returns {{
32
40
  * tier: "quick" | "simple" | "medium" | "complex",
33
41
  * reasons: string[],
34
42
  * next: string,
43
+ * suggestElicit: boolean,
35
44
  * fileCount: number,
36
45
  * }}
37
46
  */
@@ -45,6 +54,7 @@ export function classifyChange(input = {}) {
45
54
  const hasComplex = COMPLEX_SIGNALS.some((re) => re.test(haystack));
46
55
  const hasMedium = MEDIUM_SIGNALS.some((re) => re.test(haystack));
47
56
  const hasNewDep = DEPENDENCY_SIGNALS.some((re) => re.test(haystack));
57
+ const hasVague = VAGUE_SIGNALS.some((re) => re.test(haystack));
48
58
 
49
59
  if (hasComplex) {
50
60
  reasons.push("sensitive surface or architecture signal in description/paths");
@@ -103,10 +113,15 @@ export function classifyChange(input = {}) {
103
113
  'feature-init → full pipeline (+ /discuss, /plan; optional AppSec/QA on verify)',
104
114
  };
105
115
 
116
+ if (hasVague) {
117
+ reasons.push("vague delivery language — consider /elicit before /specify");
118
+ }
119
+
106
120
  return {
107
121
  tier,
108
122
  reasons,
109
123
  next: nextByTier[tier],
124
+ suggestElicit: hasVague && !hasComplex,
110
125
  fileCount,
111
126
  };
112
127
  }
@@ -123,5 +138,8 @@ export function formatClassifyChange(result) {
123
138
  ...result.reasons.map((r) => ` - ${r}`),
124
139
  `Next: ${result.next}`,
125
140
  ];
141
+ if (result.suggestElicit) {
142
+ lines.push("Suggest: /elicit (structured Q&A) or /specify if scope is already clear");
143
+ }
126
144
  return `${lines.join("\n")}\n`;
127
145
  }
package/lib/constants.js CHANGED
@@ -118,6 +118,8 @@ export const SCRIPT_ASSETS = [
118
118
  { file: "_memory_embed.py", remotePath: "scripts/_memory_embed.py" },
119
119
  { file: "episodes.py", remotePath: "scripts/episodes.py" },
120
120
  { file: "code_index.py", remotePath: "scripts/code_index.py" },
121
+ { file: "validate_req_analysis.py", remotePath: "scripts/validate_req_analysis.py" },
122
+ { file: "req_context.py", remotePath: "scripts/req_context.py" },
121
123
  ];
122
124
 
123
125
  /** @type {{ file: string, remotePath: string }[]} */
package/lib/gates.js CHANGED
@@ -34,6 +34,7 @@ const GATE_SCRIPTS = {
34
34
  "validate-state": "validate_state.py",
35
35
  "validate-traceability": "validate_traceability.py",
36
36
  "validate-quick": "validate_quick.py",
37
+ "validate-req-analysis": "validate_req_analysis.py",
37
38
  "analyze-artifacts": "analyze_artifacts.py",
38
39
  "check-commit": "check_commit.py",
39
40
  lessons: "lessons.py",
@@ -47,6 +48,7 @@ const AUX_SCRIPTS = {
47
48
  "memory-retrieve": "memory_retrieve.py",
48
49
  episodes: "episodes.py",
49
50
  "code-index": "code_index.py",
51
+ "req-context": "req_context.py",
50
52
  };
51
53
 
52
54
  const GUARDRAILS_SCRIPTS = { ...GATE_SCRIPTS, ...AUX_SCRIPTS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luizsantiago/spec-guardrails",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Spec-driven process kit for AI coding agents: write goals in .specs/, break into tasks, implement in waves, verify with proof. Process mode (Node) or Brakes mode (Node + Python gates). Works with Cursor, Claude, Copilot, and Codex.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """Assemble kickoff + requirements brief context for the Specify phase.
3
+
4
+ python3 req_context.py --scope project
5
+ python3 req_context.py --scope feature --slug settings-page
6
+ python3 req_context.py --json
7
+
8
+ Read-only: lists discovered sources and brief paths; does not mutate files.
9
+ Exit codes: 0 success, 2 usage error.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ PROJECT_DIR = Path(".specs/project")
20
+ KICKOFF_PATHS = (
21
+ Path("prd.md"),
22
+ Path("docs/brief.md"),
23
+ Path("docs/prd.md"),
24
+ PROJECT_DIR / "kickoff.md",
25
+ )
26
+ PROJECT_BRIEF = PROJECT_DIR / "requirements-brief.md"
27
+ FEATURE_BRIEFS = PROJECT_DIR / "feature-briefs"
28
+
29
+
30
+ def discover_sources(root: Path) -> list[dict[str, str | bool]]:
31
+ entries: list[dict[str, str | bool]] = []
32
+ seen: set[str] = set()
33
+ for rel in KICKOFF_PATHS:
34
+ key = rel.as_posix()
35
+ if key in seen:
36
+ continue
37
+ seen.add(key)
38
+ path = root / rel
39
+ entries.append({"path": key, "exists": path.is_file()})
40
+ return entries
41
+
42
+
43
+ def resolve_brief(root: Path, scope: str, slug: str | None) -> Path | None:
44
+ if scope == "project":
45
+ candidate = root / PROJECT_BRIEF
46
+ return candidate if candidate.is_file() else None
47
+
48
+ if not slug:
49
+ briefs_root = root / FEATURE_BRIEFS
50
+ if not briefs_root.is_dir():
51
+ return None
52
+ matches = sorted(briefs_root.rglob("requirements-brief.md"))
53
+ return matches[0] if len(matches) == 1 else None
54
+
55
+ candidate = root / FEATURE_BRIEFS / slug / "requirements-brief.md"
56
+ return candidate if candidate.is_file() else None
57
+
58
+
59
+ def excerpt(path: Path, max_chars: int = 1200) -> str:
60
+ text = path.read_text(encoding="utf-8")
61
+ trimmed = text.strip()
62
+ if len(trimmed) <= max_chars:
63
+ return trimmed
64
+ return f"{trimmed[: max_chars - 3].rstrip()}..."
65
+
66
+
67
+ def build_context(root: Path, scope: str, slug: str | None) -> dict:
68
+ sources = discover_sources(root)
69
+ brief = resolve_brief(root, scope, slug)
70
+ payload: dict = {
71
+ "scope": scope,
72
+ "sources": sources,
73
+ "brief_path": brief.relative_to(root).as_posix() if brief else None,
74
+ }
75
+ if brief:
76
+ payload["brief_excerpt"] = excerpt(brief)
77
+ if scope == "feature" and slug:
78
+ payload["slug"] = slug
79
+ return payload
80
+
81
+
82
+ def format_markdown(ctx: dict) -> str:
83
+ lines = [
84
+ "# Requirements context",
85
+ "",
86
+ f"Scope: **{ctx['scope']}**",
87
+ "",
88
+ "## Kickoff sources",
89
+ "",
90
+ ]
91
+ for entry in ctx["sources"]:
92
+ label = "found" if entry["exists"] else "missing"
93
+ lines.append(f"- [{label}] {entry['path']}")
94
+
95
+ lines.extend(["", "## Requirements brief", ""])
96
+ if ctx.get("brief_path"):
97
+ lines.append(f"- Path: `{ctx['brief_path']}`")
98
+ if ctx.get("brief_excerpt"):
99
+ lines.extend(["", "### Excerpt", "", "```markdown", ctx["brief_excerpt"], "```"])
100
+ else:
101
+ lines.append("- No requirements brief found for this scope.")
102
+
103
+ lines.extend(
104
+ [
105
+ "",
106
+ "## Next",
107
+ "",
108
+ "- Run `validate-req-analysis` on the brief before `/specify`",
109
+ "- Derive spec.md from the brief — do not re-ask resolved questions",
110
+ ]
111
+ )
112
+ return "\n".join(lines) + "\n"
113
+
114
+
115
+ def main(argv: list[str] | None = None) -> int:
116
+ parser = argparse.ArgumentParser(description="Requirements context for Specify")
117
+ parser.add_argument(
118
+ "--scope",
119
+ choices=("project", "feature"),
120
+ default="project",
121
+ help="Elicitation scope (default: project)",
122
+ )
123
+ parser.add_argument(
124
+ "--slug",
125
+ help="Feature slug when scope=feature (default: sole feature brief if only one exists)",
126
+ )
127
+ parser.add_argument("--json", action="store_true", help="Emit JSON instead of markdown")
128
+ args = parser.parse_args(argv)
129
+
130
+ if args.scope == "feature" and args.slug and not args.slug.strip():
131
+ print("[req-context] USAGE - --slug must not be empty", file=sys.stderr)
132
+ return 2
133
+
134
+ ctx = build_context(Path("."), args.scope, args.slug.strip() if args.slug else None)
135
+
136
+ if args.json:
137
+ print(json.dumps(ctx, indent=2))
138
+ else:
139
+ print(format_markdown(ctx), end="")
140
+
141
+ return 0
142
+
143
+
144
+ if __name__ == "__main__":
145
+ raise SystemExit(main())
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """Structural gate for requirements briefs produced by /elicit.
3
+
4
+ python3 validate_req_analysis.py .specs/project/requirements-brief.md
5
+ python3 validate_req_analysis.py .specs/project/feature-briefs/settings-page/requirements-brief.md
6
+
7
+ Checks (markdown structure only):
8
+ * Required sections: Goal, Context sources, Owner approval
9
+ * Context sources lists at least one non-placeholder entry
10
+ * Open questions is empty or explicitly "- none"
11
+ * Owner approval indicates yes with a date
12
+ * No [NEEDS CLARIFICATION] or [OPEN QUESTION] markers remain
13
+
14
+ Exit codes: 0 pass, 1 blocking issues, 2 usage error.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ from _common import Report, visible_markdown
25
+
26
+ GATE = "validate-req-analysis"
27
+ PROJECT_DIR = Path(".specs/project")
28
+ PROJECT_BRIEF = PROJECT_DIR / "requirements-brief.md"
29
+ FEATURE_BRIEFS = PROJECT_DIR / "feature-briefs"
30
+
31
+ HEADING = re.compile(r"^##\s+(.+?)\s*$", re.MULTILINE)
32
+ APPROVAL_YES = re.compile(
33
+ r"\b(?:approved|approval)\b[^\n]*:\s*(?:yes|approved|true)\b",
34
+ re.IGNORECASE,
35
+ )
36
+ APPROVAL_DATE = re.compile(
37
+ r"\bdate\b\s*:\s*(\d{4}-\d{2}-\d{2}|YYYY-MM-DD)",
38
+ re.IGNORECASE,
39
+ )
40
+ OPEN_NONE = re.compile(r"^\s*[-*]\s*(?:none|n/a)\s*$", re.IGNORECASE | re.MULTILINE)
41
+ CLARIFICATION = re.compile(r"\[(?:NEEDS CLARIFICATION|OPEN QUESTION)\]", re.IGNORECASE)
42
+ PLACEHOLDER_SOURCE = re.compile(
43
+ r"^\s*[-*]\s*\(\s*list every file",
44
+ re.IGNORECASE | re.MULTILINE,
45
+ )
46
+
47
+
48
+ def _fail_usage(message: str, target: str = ".") -> None:
49
+ print(f"[{GATE}] USAGE - {message}", file=sys.stderr)
50
+ raise SystemExit(2)
51
+
52
+
53
+ def resolve_brief_path(raw: str | None, root: Path = Path(".")) -> Path:
54
+ if not raw:
55
+ candidate = root / PROJECT_BRIEF
56
+ if candidate.is_file():
57
+ return candidate
58
+ _fail_usage(
59
+ "no brief path given and .specs/project/requirements-brief.md is missing",
60
+ str(PROJECT_BRIEF),
61
+ )
62
+
63
+ candidate = Path(raw).expanduser()
64
+ if candidate.is_absolute():
65
+ brief = candidate
66
+ else:
67
+ brief = root / candidate
68
+
69
+ if not brief.is_file():
70
+ _fail_usage(f"no such requirements brief: {raw}", raw or str(PROJECT_BRIEF))
71
+
72
+ rel = brief.resolve()
73
+ project_root = (root / PROJECT_DIR).resolve()
74
+ try:
75
+ rel.relative_to(project_root)
76
+ except ValueError:
77
+ _fail_usage(
78
+ f"brief must live under {PROJECT_DIR.as_posix()}/",
79
+ str(brief),
80
+ )
81
+
82
+ if brief.name != "requirements-brief.md":
83
+ _fail_usage(
84
+ "expected a requirements-brief.md file under .specs/project/",
85
+ str(brief),
86
+ )
87
+
88
+ return brief
89
+
90
+
91
+ def section_body(text: str, title: str) -> str:
92
+ match = HEADING.search(text)
93
+ if not match:
94
+ return ""
95
+
96
+ pattern = re.compile(
97
+ rf"^##\s+{re.escape(title)}\s*$",
98
+ re.MULTILINE | re.IGNORECASE,
99
+ )
100
+ start = pattern.search(text)
101
+ if not start:
102
+ return ""
103
+
104
+ rest = text[start.end() :]
105
+ next_heading = HEADING.search(rest)
106
+ body = rest[: next_heading.start()] if next_heading else rest
107
+ return body.strip()
108
+
109
+
110
+ def build_report(brief_path: Path) -> Report:
111
+ report = Report(GATE, brief_path.as_posix())
112
+ text = brief_path.read_text(encoding="utf-8")
113
+ visible = visible_markdown(text)
114
+
115
+ for section in ("Goal", "Context sources", "Owner approval"):
116
+ if not section_body(text, section):
117
+ report.error(f"missing ## {section} section")
118
+
119
+ goal_body = section_body(text, "Goal")
120
+ if goal_body and len(goal_body.strip()) < 8:
121
+ report.error("Goal section is too short to be actionable")
122
+
123
+ sources_body = section_body(text, "Context sources")
124
+ if sources_body:
125
+ if PLACEHOLDER_SOURCE.search(sources_body):
126
+ report.error("Context sources still contains the scaffold placeholder")
127
+ elif not re.search(r"^\s*[-*]\s+\S", sources_body, re.MULTILINE):
128
+ report.error("Context sources must list at least one bullet entry")
129
+ else:
130
+ report.ok("Context sources lists at least one entry")
131
+ else:
132
+ report.error("Context sources section is empty")
133
+
134
+ open_body = section_body(text, "Open questions")
135
+ if open_body:
136
+ bullets = [
137
+ line.strip()
138
+ for line in open_body.splitlines()
139
+ if line.strip().startswith(("-", "*"))
140
+ ]
141
+ if bullets and not all(OPEN_NONE.match(line) for line in bullets):
142
+ report.error(
143
+ 'Open questions must be "- none" or empty before /specify'
144
+ )
145
+ else:
146
+ report.ok("Open questions closed")
147
+ else:
148
+ report.ok("Open questions section absent or empty")
149
+
150
+ approval_body = section_body(text, "Owner approval")
151
+ if approval_body:
152
+ if not APPROVAL_YES.search(approval_body):
153
+ report.error('Owner approval must include "Approved: yes" (or equivalent)')
154
+ if APPROVAL_DATE.search(approval_body) and "YYYY-MM-DD" in approval_body:
155
+ report.error("Owner approval date is still the scaffold placeholder")
156
+ elif not re.search(r"\d{4}-\d{2}-\d{2}", approval_body):
157
+ report.warn("Owner approval has no YYYY-MM-DD date")
158
+ else:
159
+ report.ok("Owner approval recorded")
160
+ else:
161
+ report.error("Owner approval section is empty")
162
+
163
+ if CLARIFICATION.search(visible):
164
+ report.error("[NEEDS CLARIFICATION] or [OPEN QUESTION] marker remains")
165
+ else:
166
+ report.ok("no clarification markers in brief")
167
+
168
+ return report
169
+
170
+
171
+ def main(argv: list[str] | None = None) -> int:
172
+ parser = argparse.ArgumentParser(description="Validate a requirements brief from /elicit")
173
+ parser.add_argument(
174
+ "brief",
175
+ nargs="?",
176
+ help="Path to requirements-brief.md (default: .specs/project/requirements-brief.md)",
177
+ )
178
+ parser.add_argument(
179
+ "--strict",
180
+ action="store_true",
181
+ help="Treat warnings as blocking failures",
182
+ )
183
+ args = parser.parse_args(argv)
184
+
185
+ brief = resolve_brief_path(args.brief)
186
+ report = build_report(brief)
187
+ return report.emit(strict=args.strict)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ raise SystemExit(main())
@@ -82,7 +82,7 @@ EXPLORE (optional) → ELICIT (optional) → SPECIFY → DISCUSS (conditional)
82
82
  | Phase | Required | Reference | Sister skill | Gate |
83
83
  | --- | --- | --- | --- | --- |
84
84
  | **Explore** | Optional | `references/explore.md` | — | — |
85
- | **Elicit** | Optional | `references/elicitation.md` | — | — (v1 skill checklist; gate in 4.x wave 2) |
85
+ | **Elicit** | Optional | `references/elicitation.md` | `validate-req-analysis` | — (before `/specify`; suggest-only entry) |
86
86
  | **Constitution** | Once per project | `references/constitution.md` | — | — |
87
87
  | **Specify** | Yes | `references/specify.md` | — | `validate_spec.py` |
88
88
  | **Discuss** | Conditional | `references/discuss.md` | — | — |
@@ -136,8 +136,14 @@ Use the template from `req-analysis init`. Required sections:
136
136
 
137
137
  Present the brief summary. Wait for explicit approval before `/specify`.
138
138
 
139
- **v1 checklist (no Python gate yet):**
139
+ **Gate before `/specify` (Brakes mode):**
140
140
 
141
+ ```bash
142
+ npx @luizsantiago/spec-guardrails req-analysis validate
143
+ # or: validate-req-analysis .specs/project/requirements-brief.md
144
+ ```
145
+
146
+ **Checklist (Process mode — agent verifies when Python is unavailable):**
141
147
  - [ ] Open questions is `- none` or empty
142
148
  - [ ] Owner approval filled
143
149
  - [ ] Context sources lists at least one input
@@ -160,6 +166,12 @@ Copy or link brief → `.specs/features/NNN-slug/requirements-brief.md` (optiona
160
166
 
161
167
  Open `specify.md` — derive `spec.md` from brief; do **not** re-ask resolved questions.
162
168
 
169
+ Optional context bundle for Specify:
170
+
171
+ ```bash
172
+ npx @luizsantiago/spec-guardrails req-analysis context --scope feature --slug settings-page
173
+ ```
174
+
163
175
  ```bash
164
176
  npx @luizsantiago/spec-guardrails req-analysis promote --scope feature
165
177
  ```