@luizsantiago/spec-guardrails 3.1.8 → 3.1.10

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
@@ -91,11 +91,14 @@ Install Python when you want gates to fire; run `doctor` to confirm Brakes are a
91
91
  | --- | --- | --- | --- |
92
92
  | Intent exists before code | `validate-spec` | Brakes | Hard gate |
93
93
  | Tasks derive from requirements | `analyze-artifacts` | Brakes | Hard gate |
94
+ | Task shape and graph when needed | `validate-tasks` | Brakes | Hard gate |
94
95
  | Requirements stay traceable | `validate-traceability` | Brakes | Hard gate |
96
+ | Quick evidence is complete | `validate-quick` | Brakes | Hard gate |
95
97
  | Dependencies respected in Execute | `loop-plan` | Brakes | Hard gate |
96
98
  | Parallel work is file-safe | `task-graph.md` + `validate-tasks` | Process + Brakes | Artifact + gate |
97
99
  | Completion cites evidence | `validate-state` | Brakes | Hard gate |
98
100
  | Commits follow policy | `check-commit` | Brakes | Hard gate |
101
+ | Lessons grounded after FAIL | `lessons` | Brakes | Hard gate |
99
102
  | Verification is independent | `/verify` + `validate.md` | Process | Phase skill |
100
103
  | Knowledge survives chats | `.specs/` + `archive-feature` | Process | Install + CLI |
101
104
 
package/lib/archive.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "./delta-merge.js";
9
9
  import { ensureDir, readFileSafe, writeFileSafe } from "./fs-utils.js";
10
10
  import { runGate } from "./gates.js";
11
+ import { assertSafeDomainSlug } from "./slug-utils.js";
11
12
  import {
12
13
  featureDir,
13
14
  readFeatureArtifact,
@@ -112,7 +113,8 @@ async function resetState(cwd) {
112
113
  */
113
114
  export function inferDomainFromFeature(featureId) {
114
115
  const match = /^(\d{3})-(.+)$/.exec(featureId);
115
- return match ? match[2] : featureId;
116
+ const slug = match ? match[2] : featureId;
117
+ return assertSafeDomainSlug(slug);
116
118
  }
117
119
 
118
120
  /**
@@ -178,7 +180,9 @@ export async function archiveFeature(featureArg, options = {}) {
178
180
  let mergeSummary = [];
179
181
 
180
182
  if (!options.skipDomainMerge) {
181
- const domain = options.domain ?? inferDomainFromFeature(featureId);
183
+ const domain = options.domain
184
+ ? assertSafeDomainSlug(options.domain)
185
+ : inferDomainFromFeature(featureId);
182
186
  const domainDir = path.join(cwd, ".specs/domains", domain);
183
187
  const domainSpecPath = path.join(domainDir, "spec.md");
184
188
  domainRelPath = `.specs/domains/${domain}/spec.md`;
package/lib/brownfield.js CHANGED
@@ -5,6 +5,7 @@ import { domainSpecStub } from "./delta-merge.js";
5
5
  import { ensureDir, readFileSafe, writeFileIfMissing, writeFileSafe } from "./fs-utils.js";
6
6
  import { initGuardrailsMemory } from "./memory.js";
7
7
  import { initProjectConfig } from "./presets.js";
8
+ import { assertSafeDomainSlug, slugifyDomain } from "./slug-utils.js";
8
9
 
9
10
  const SKIP_DIRS = new Set([
10
11
  ".git",
@@ -49,18 +50,6 @@ Track milestones and archived features.
49
50
 
50
51
  `;
51
52
 
52
- /**
53
- * @param {string} name
54
- * @returns {string}
55
- */
56
- function slugifyDomain(name) {
57
- return name
58
- .toLowerCase()
59
- .replace(/[^a-z0-9]+/g, "-")
60
- .replace(/^-+|-+$/g, "")
61
- .slice(0, 48);
62
- }
63
-
64
53
  /**
65
54
  * @param {string} cwd
66
55
  * @returns {Promise<string>}
@@ -345,7 +334,7 @@ export async function projectInit(options = {}) {
345
334
  const repoName = await readRepoName(cwd);
346
335
 
347
336
  let domains = options.domains?.map((d) => ({
348
- domain: slugifyDomain(d),
337
+ domain: assertSafeDomainSlug(d),
349
338
  hint: `(manual: ${d})`,
350
339
  }));
351
340
 
package/lib/doctor.js CHANGED
@@ -18,8 +18,6 @@ const execFileAsync = promisify(execFile);
18
18
  export const DOCTOR_PROCESS_CHECK_IDS = [
19
19
  "skills-hub",
20
20
  "specs-scaffold",
21
- "config",
22
- "baseline-rule",
23
21
  "state-feature",
24
22
  "platform-adapters",
25
23
  ];
@@ -170,14 +168,16 @@ export async function runDoctorChecks(cwd) {
170
168
  weight: 10,
171
169
  pass: await pathExists(cwd, ".specs/config.yaml"),
172
170
  suggest: NPX("init-config --preset default"),
171
+ optional: true,
173
172
  });
174
173
 
175
174
  checks.push({
176
175
  id: "baseline-rule",
177
- label: "engineering-baseline.mdc always-on rule",
176
+ label: "engineering-baseline.mdc always-on rule (Cursor)",
178
177
  weight: 8,
179
178
  pass: await pathExists(cwd, ".cursor/rules/engineering-baseline.mdc"),
180
179
  suggest: NPX("install"),
180
+ optional: true,
181
181
  });
182
182
 
183
183
  checks.push({
package/lib/gates.js CHANGED
@@ -7,12 +7,25 @@ import { NPX, GUARDRAILS_SCRIPTS_DIR } from "./constants.js";
7
7
 
8
8
  /** @typedef {{ command: string, args: string[] }} PythonInterpreter */
9
9
 
10
- const PYTHON_CANDIDATES = [
10
+ const PYTHON_CANDIDATES_UNIX = [
11
11
  { command: "python3", args: [] },
12
12
  { command: "python", args: [] },
13
13
  { command: "py", args: ["-3"] },
14
14
  ];
15
15
 
16
+ const PYTHON_CANDIDATES_WINDOWS = [
17
+ { command: "py", args: ["-3"] },
18
+ { command: "python", args: [] },
19
+ { command: "python3", args: [] },
20
+ ];
21
+
22
+ /**
23
+ * @returns {PythonInterpreter[]}
24
+ */
25
+ export function getPythonCandidates() {
26
+ return process.platform === "win32" ? PYTHON_CANDIDATES_WINDOWS : PYTHON_CANDIDATES_UNIX;
27
+ }
28
+
16
29
  const MIN_PYTHON = [3, 10];
17
30
 
18
31
  const GATE_SCRIPTS = {
@@ -57,7 +70,7 @@ function run(command, args, options = {}) {
57
70
  * @returns {[number, number] | null}
58
71
  */
59
72
  export function parsePythonVersion(output) {
60
- const match = output.match(/(\d+)\.(\d+)/);
73
+ const match = output.match(/^Python\s+(\d+)\.(\d+)/im);
61
74
  if (!match) {
62
75
  return null;
63
76
  }
@@ -123,7 +136,7 @@ export async function resolveScriptsDir(_cwd) {
123
136
  * @returns {Promise<PythonInterpreter | null>}
124
137
  */
125
138
  export async function resolvePython() {
126
- for (const candidate of PYTHON_CANDIDATES) {
139
+ for (const candidate of getPythonCandidates()) {
127
140
  try {
128
141
  const output = await readPythonVersion(candidate);
129
142
  const version = parsePythonVersion(output);
@@ -0,0 +1,40 @@
1
+ /** @typedef {string} DomainSlug */
2
+
3
+ export const FEATURE_ID_PATTERN = /^\d{3}-[a-z0-9][a-z0-9-]*$/;
4
+
5
+ /**
6
+ * @param {string} id
7
+ * @returns {boolean}
8
+ */
9
+ export function isValidFeatureId(id) {
10
+ return FEATURE_ID_PATTERN.test(id);
11
+ }
12
+
13
+ /**
14
+ * @param {string} name
15
+ * @returns {DomainSlug}
16
+ */
17
+ export function slugifyDomain(name) {
18
+ return name
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9]+/g, "-")
21
+ .replace(/^-+|-+$/g, "")
22
+ .slice(0, 48);
23
+ }
24
+
25
+ const DOMAIN_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
26
+
27
+ /**
28
+ * @param {string} raw
29
+ * @returns {DomainSlug}
30
+ */
31
+ export function assertSafeDomainSlug(raw) {
32
+ if (/[/\\]|\.\./.test(raw)) {
33
+ throw new Error(`Invalid domain slug: ${raw}`);
34
+ }
35
+ const slug = slugifyDomain(raw);
36
+ if (!slug || !DOMAIN_SLUG_PATTERN.test(slug)) {
37
+ throw new Error(`Invalid domain slug: ${raw}`);
38
+ }
39
+ return slug;
40
+ }
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  import { readFileSafe } from "./fs-utils.js";
5
+ import { isValidFeatureId } from "./slug-utils.js";
5
6
 
6
7
  const FEATURES_DIR = ".specs/features";
7
8
 
@@ -15,7 +16,7 @@ export async function listFeatureIds(cwd) {
15
16
  try {
16
17
  const entries = await fs.readdir(featuresRoot, { withFileTypes: true });
17
18
  return entries
18
- .filter((entry) => entry.isDirectory())
19
+ .filter((entry) => entry.isDirectory() && isValidFeatureId(entry.name))
19
20
  .map((entry) => entry.name)
20
21
  .sort();
21
22
  } catch (err) {
@@ -26,6 +27,38 @@ export async function listFeatureIds(cwd) {
26
27
  }
27
28
  }
28
29
 
30
+ /**
31
+ * @param {string} trimmed
32
+ * @param {string} cwd
33
+ * @returns {Promise<string>}
34
+ */
35
+ async function resolveFeatureIdFromPath(trimmed, cwd) {
36
+ const asPath = path.resolve(cwd, trimmed);
37
+ const featuresRoot = path.resolve(cwd, FEATURES_DIR);
38
+ const relative = path.relative(featuresRoot, asPath);
39
+
40
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
41
+ throw new Error(`No such feature or path: ${trimmed}`);
42
+ }
43
+
44
+ const featureId = relative.split(path.sep)[0];
45
+ if (!isValidFeatureId(featureId)) {
46
+ throw new Error(`No such feature or path: ${trimmed}`);
47
+ }
48
+
49
+ const named = path.join(featuresRoot, featureId);
50
+ try {
51
+ const stat = await fs.stat(named);
52
+ if (stat.isDirectory()) {
53
+ return featureId;
54
+ }
55
+ } catch {
56
+ // fall through
57
+ }
58
+
59
+ throw new Error(`No such feature or path: ${trimmed}`);
60
+ }
61
+
29
62
  /**
30
63
  * @param {string | undefined} raw
31
64
  * @param {string} cwd
@@ -34,13 +67,13 @@ export async function listFeatureIds(cwd) {
34
67
  export async function resolveFeatureId(raw, cwd) {
35
68
  if (raw?.trim()) {
36
69
  const trimmed = raw.trim();
37
- const asPath = path.resolve(cwd, trimmed);
38
70
 
39
71
  if (trimmed.endsWith(".md") || trimmed.includes("/") || trimmed.includes("\\")) {
40
- const relative = path.relative(path.join(cwd, FEATURES_DIR), asPath);
41
- if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
42
- return relative.split(path.sep)[0];
43
- }
72
+ return resolveFeatureIdFromPath(trimmed, cwd);
73
+ }
74
+
75
+ if (!isValidFeatureId(trimmed)) {
76
+ throw new Error(`Invalid feature id: ${trimmed} (expected NNN-slug)`);
44
77
  }
45
78
 
46
79
  const named = path.join(cwd, FEATURES_DIR, trimmed);
@@ -58,6 +91,9 @@ export async function resolveFeatureId(raw, cwd) {
58
91
 
59
92
  const active = await readActiveFeatureFromState(cwd);
60
93
  if (active && active !== "—") {
94
+ if (!isValidFeatureId(active)) {
95
+ throw new Error(`Invalid active feature in STATE.md: ${active}`);
96
+ }
61
97
  return active;
62
98
  }
63
99
 
@@ -108,6 +144,9 @@ export async function readActiveFeatureFromState(cwd) {
108
144
  * @returns {string}
109
145
  */
110
146
  export function featureDir(featureId, cwd) {
147
+ if (!isValidFeatureId(featureId)) {
148
+ throw new Error(`Invalid feature id: ${featureId} (expected NNN-slug)`);
149
+ }
111
150
  return path.join(cwd, FEATURES_DIR, featureId);
112
151
  }
113
152
 
@@ -118,6 +157,5 @@ export function featureDir(featureId, cwd) {
118
157
  * @returns {Promise<string>}
119
158
  */
120
159
  export async function readFeatureArtifact(featureId, cwd, filename) {
121
- const filePath = path.join(featureDir(featureId, cwd), filename);
122
- return readFileSafe(filePath);
160
+ return readFileSafe(path.join(featureDir(featureId, cwd), filename));
123
161
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luizsantiago/spec-guardrails",
3
- "version": "3.1.8",
3
+ "version": "3.1.10",
4
4
  "description": "Keep AI coding agents honest — specify the work, prove each step, verify independently. Process mode (Node) for flexibility; Brakes mode (Node + Python) for structural gates and a Guarantees matrix. Progressive loading, independent verify — any AI agent.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "scripts": {
13
13
  "guardrails": "node index.js",
14
14
  "test": "npm run test:node && npm run test:gates",
15
- "test:node": "node --test test/install.test.js test/test_feature_init.test.js test/test_config.test.js test/test_archive.test.js test/test_delta_merge.test.js test/test_presets.test.js test/test_brownfield.test.js test/test_doctor.test.js test/test_token_cost.test.js test/test_next_steps.test.js test/test_classify_change.test.js test/test_feature_status.test.js test/test_agent_contract.test.js test/test_gates_python.test.js",
15
+ "test:node": "node --test test/install.test.js test/test_feature_init.test.js test/test_config.test.js test/test_archive.test.js test/test_delta_merge.test.js test/test_presets.test.js test/test_brownfield.test.js test/test_doctor.test.js test/test_token_cost.test.js test/test_next_steps.test.js test/test_classify_change.test.js test/test_feature_status.test.js test/test_agent_contract.test.js test/test_gates_python.test.js test/test_specs_utils.test.js",
16
16
  "test:gates": "node test/run-gate-tests.mjs",
17
17
  "prepublishOnly": "npm test"
18
18
  },
@@ -96,6 +96,8 @@ class Report:
96
96
 
97
97
  FEATURES_DIR = Path(".specs/features")
98
98
 
99
+ FEATURE_ID_PATTERN = re.compile(r"^\d{3}-[a-z0-9][a-z0-9-]*$")
100
+
99
101
  REQUIREMENT_ID = re.compile(
100
102
  r"^(?P<level>#{2,6})\s*(?P<id>[A-Z][A-Z0-9]{1,9}-\d{2,4})\b",
101
103
  re.MULTILINE,
@@ -215,6 +217,22 @@ def _fail_usage(gate: str, target: str, message: str) -> None:
215
217
  sys.exit(EXIT_USAGE)
216
218
 
217
219
 
220
+ def is_valid_feature_id(feature_id: str) -> bool:
221
+ return bool(FEATURE_ID_PATTERN.match(feature_id))
222
+
223
+
224
+ def _feature_dir_under_root(feature_dir: Path, root: Path) -> bool:
225
+ features_root = (root / FEATURES_DIR).resolve()
226
+ try:
227
+ resolved = feature_dir.resolve()
228
+ relative = resolved.relative_to(features_root)
229
+ except ValueError:
230
+ return False
231
+ if not relative.parts:
232
+ return False
233
+ return is_valid_feature_id(relative.parts[0])
234
+
235
+
218
236
  def list_features(root: Path = Path(".")) -> list[Path]:
219
237
  """Return every feature directory under `.specs/features`, sorted by name."""
220
238
 
@@ -223,7 +241,11 @@ def list_features(root: Path = Path(".")) -> list[Path]:
223
241
  if not base.is_dir():
224
242
  return []
225
243
 
226
- return sorted(path for path in base.iterdir() if path.is_dir())
244
+ return sorted(
245
+ path
246
+ for path in base.iterdir()
247
+ if path.is_dir() and is_valid_feature_id(path.name)
248
+ )
227
249
 
228
250
 
229
251
  def resolve_feature_dir(
@@ -231,19 +253,38 @@ def resolve_feature_dir(
231
253
  ) -> Path:
232
254
  """Resolve a feature directory from a path, a bare feature name, or context.
233
255
 
234
- Accepts `.specs/features/auth/spec.md`, `.specs/features/auth`, `auth`, or
235
- nothing at all when the project has exactly one feature.
256
+ Accepts `.specs/features/001-auth/spec.md`, `.specs/features/001-auth`,
257
+ `001-auth`, or nothing at all when the project has exactly one feature.
236
258
  """
237
259
 
238
260
  if raw:
261
+ if raw in (".", "..") or ".." in Path(raw).parts:
262
+ _fail_usage(
263
+ gate,
264
+ raw,
265
+ f"invalid feature id: {raw} (expected NNN-slug)",
266
+ )
267
+
239
268
  candidate = Path(raw).expanduser()
240
269
 
241
270
  if candidate.is_file():
242
- return candidate.parent
271
+ feature_dir = candidate.parent
272
+ if not _feature_dir_under_root(feature_dir, root):
273
+ _fail_usage(gate, raw, f"no such feature or path: {raw}")
274
+ return feature_dir
243
275
 
244
276
  if candidate.is_dir():
277
+ if not _feature_dir_under_root(candidate, root):
278
+ _fail_usage(gate, raw, f"no such feature or path: {raw}")
245
279
  return candidate
246
280
 
281
+ if not is_valid_feature_id(raw):
282
+ _fail_usage(
283
+ gate,
284
+ raw,
285
+ f"invalid feature id: {raw} (expected NNN-slug)",
286
+ )
287
+
247
288
  named = root / FEATURES_DIR / raw
248
289
  if named.is_dir():
249
290
  return named
@@ -22,7 +22,7 @@ import re
22
22
  import sys
23
23
  from pathlib import Path
24
24
 
25
- from _common import Report, visible_markdown
25
+ from _common import Report, is_valid_feature_id, visible_markdown
26
26
 
27
27
  GATE = "validate-quick"
28
28
  QUICK_DIR = Path(".specs/quick")
@@ -43,38 +43,65 @@ EVIDENCE_LINE = re.compile(
43
43
  )
44
44
 
45
45
 
46
+ def _fail_usage(message: str, target: str = ".") -> None:
47
+ print(f"[{GATE}] USAGE - {message}", file=sys.stderr)
48
+ raise SystemExit(2)
49
+
50
+
51
+ def _quick_dir_under_root(quick_dir: Path, root: Path) -> bool:
52
+ quick_root = (root / QUICK_DIR).resolve()
53
+ try:
54
+ resolved = quick_dir.resolve()
55
+ relative = resolved.relative_to(quick_root)
56
+ except ValueError:
57
+ return False
58
+ if not relative.parts:
59
+ return False
60
+ return is_valid_feature_id(relative.parts[0])
61
+
62
+
46
63
  def resolve_quick_dir(raw: str | None, root: Path = Path(".")) -> Path:
47
64
  if raw:
65
+ if raw in (".", "..") or ".." in Path(raw).parts:
66
+ _fail_usage(f"invalid quick id: {raw} (expected NNN-slug)", raw)
67
+
48
68
  candidate = Path(raw).expanduser()
69
+
70
+ if candidate.is_file():
71
+ quick_dir = candidate.parent
72
+ if not _quick_dir_under_root(quick_dir, root):
73
+ _fail_usage(f"no such quick folder: {raw}", raw)
74
+ return quick_dir
75
+
49
76
  if candidate.is_dir():
77
+ if not _quick_dir_under_root(candidate, root):
78
+ _fail_usage(f"no such quick folder: {raw}", raw)
50
79
  return candidate
80
+
81
+ if not is_valid_feature_id(raw):
82
+ _fail_usage(f"invalid quick id: {raw} (expected NNN-slug)", raw)
83
+
51
84
  named = root / QUICK_DIR / raw
52
85
  if named.is_dir():
53
86
  return named
54
- print(f"[{GATE}] USAGE - no such quick folder: {raw}", file=sys.stderr)
55
- raise SystemExit(2)
87
+
88
+ _fail_usage(f"no such quick folder: {raw}", raw)
56
89
 
57
90
  base = root / QUICK_DIR
58
91
  if not base.is_dir():
59
- print(
60
- f"[{GATE}] USAGE - {base} missing — create .specs/quick/NNN-slug/ first",
61
- file=sys.stderr,
62
- )
63
- raise SystemExit(2)
92
+ _fail_usage(f"{base} missing — create .specs/quick/NNN-slug/ first", str(base))
64
93
 
65
- folders = sorted(p for p in base.iterdir() if p.is_dir())
94
+ folders = sorted(
95
+ p for p in base.iterdir() if p.is_dir() and is_valid_feature_id(p.name)
96
+ )
66
97
  if len(folders) == 1:
67
98
  return folders[0]
68
99
  if not folders:
69
- print(f"[{GATE}] USAGE - no quick folders under {base}", file=sys.stderr)
70
- raise SystemExit(2)
100
+ _fail_usage(f"no quick folders under {base}", str(base))
71
101
 
72
102
  listed = "\n".join(f" {p.name}" for p in folders)
73
- print(
74
- f"[{GATE}] USAGE - {len(folders)} quick folders — name one:\n{listed}",
75
- file=sys.stderr,
76
- )
77
- raise SystemExit(2)
103
+ _fail_usage(f"{len(folders)} quick folders — name one:\n{listed}", str(base))
104
+ raise AssertionError("unreachable")
78
105
 
79
106
 
80
107
  def field_map(text: str) -> dict[str, str]:
@@ -80,7 +80,7 @@ EXPLORE (optional) → SPECIFY → DISCUSS (conditional) → DESIGN (optional)
80
80
  | **Archive** | After Verify PASS | `references/archive.md` | `git-handoff.md` | `archive-feature` |
81
81
  | **Converge** | On drift | `references/converge.md` | — | `analyze_artifacts.py` |
82
82
  | **Handoff** | Yes | `references/memory.md` | `git-handoff.md` | — |
83
- | **Quick** | Alternative | `references/quick-mode.md` | — | `check_commit.py` |
83
+ | **Quick** | Alternative | `references/quick-mode.md` | — | `check_commit.py`, `validate_quick.py` |
84
84
  | **Context** | Always | `references/context-limits.md` | — | — |
85
85
  | **Sub-agents** | When batched | `references/sub-agents.md` | `task-graph-engineering.md` | — |
86
86
  | **Lessons** | On FAIL | `references/lessons.md` | — | `lessons.py` |
@@ -104,7 +104,7 @@ Complexity determines depth. Do not run every phase on every change.
104
104
 
105
105
  | Tier | Scope | Path |
106
106
  | --- | --- | --- |
107
- | **Quick** | ≤3 files, no design decisions, no new dependencies | `references/quick-mode.md` — describe, implement, verify, commit; gate: `validate-quick` |
107
+ | **Quick** | ≤3 files, no design decisions, no new dependencies | `references/quick-mode.md` — describe, implement, verify, commit; gates: `check_commit.py` + `validate-quick` |
108
108
  | **Simple** | 2–5 files, localized change | Specify → Execute → Verify |
109
109
  | **Medium** | New feature, <10 tasks | Specify → Tasks → Execute → Verify |
110
110
  | **Complex** | New architecture, API surface, infra | Specify → Discuss → Design → Tasks → Execute → Verify |
@@ -114,7 +114,7 @@ Complexity determines depth. Do not run every phase on every change.
114
114
 
115
115
  **Rules**
116
116
 
117
- - **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.
117
+ - **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 `check_commit.py` on each commit and `validate-quick` as the close/evidence gate.
118
118
  - **Design is skipped** when there are no architectural decisions and no new patterns.
119
119
  - **Tasks is skipped** when there are ≤3 obvious steps.
120
120
  - **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.
@@ -20,7 +20,7 @@ You installed the **Spec Guardrails**. You do **not** need to memorize CLI comma
20
20
 
21
21
  ## Agent commands (chat — not terminal)
22
22
 
23
- Type these in **chat** in your agent environment. They load phase procedures from the installed skills tree (e.g. `.cursor/skills/references/`). In **Brakes mode**, the agent runs Python gates for you.
23
+ Type these in **chat** in your agent environment. They load phase procedures from the installed skills tree (e.g. `.cursor/skills/references/`, `.github/skills/references/`, `.codex/skills/references/` — use the tree your agent loads). In **Brakes mode**, the agent runs Python gates for you.
24
24
 
25
25
  | Command | When |
26
26
  | --- | --- |
@@ -55,7 +55,8 @@ Everything else (`loop-plan`, `validate-tasks`, `check-commit`, …) is normally
55
55
 
56
56
  | Path | Purpose |
57
57
  | --- | --- |
58
- | `.cursor/skills/agent-architecture.md` | Hub — phase map |
58
+ | `*/skills/agent-architecture.md` | Hub — phase map (`.cursor/`, `.claude/`, `.github/`, `.codex/`) |
59
+ | Root `AGENTS.md` | Open-standard entry |
59
60
  | `.specs/STATE.md` | Where you left off |
60
61
  | `.specs/features/` | One folder per feature |
61
62
  | `.specs/guardrails/scripts/` | Automatic gates |