@luizsantiago/spec-guardrails 3.1.4 → 3.1.8

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
@@ -32,12 +32,12 @@ npx @luizsantiago/spec-guardrails install
32
32
 
33
33
  | Lands in your project | Purpose |
34
34
  | --- | --- |
35
- | `.cursor/skills/` + `.claude/skills/` | Hub, phase references, sister skills (first shipped **adapters** — not the only supported agents) |
35
+ | `.cursor/skills/` + `.claude/skills/` + `.github/skills/` + `.codex/skills/` | Hub, phase references, sister skills (**shipped adapters** — same content, product-specific paths) |
36
36
  | `.specs/guardrails/scripts/` | Python gate scripts (Brakes mode) |
37
37
  | `.specs/STATE.md`, `.specs/features/`, … | Project memory (any agent) |
38
38
  | `.cursor/rules/engineering-baseline.mdc` | Always-on Cursor rule |
39
39
 
40
- **Agent environments:** the **core** (`.specs/`, CLI, hub, Python gates) works with any AI agent that reads repo instructions and runs shell commands. Install ships Cursor + Claude Code adapters today; Codex, GitHub Copilot, and others are documented in [Architecture](docs/guide/Architecture.md).
40
+ **Agent environments:** the **core** (`.specs/`, CLI, hub, Python gates) works with any AI agent. **Install** ships adapters for **Cursor, Claude Code, GitHub Copilot, and OpenAI Codex** (plus root `AGENTS.md`). See [Architecture](docs/guide/Architecture.md).
41
41
 
42
42
  Re-run `install` anytime to refresh skills; your `.specs/` decisions and `STATE.md` are kept.
43
43
 
@@ -60,7 +60,7 @@ Four ideas stack — full explanation: **[Concepts](docs/guide/concepts.md)**
60
60
  | **Brakes / Gates** | Structural stop-gates | Python scripts exit non-zero when paperwork or evidence is missing |
61
61
  | **Loop** | Execute in waves | `loop-plan` picks the next jobs; sub-agents when files don’t overlap |
62
62
  | **Graph** | Parallel task map | `task-graph.md` — safe parallelism without file collisions |
63
- | **Memory** | Repo-local state | `.specs/` — specs, decisions, and handoff survive across chats |
63
+ | **Memory** | Persistent project state | `.specs/` — specs, decisions, and handoff survive across chats |
64
64
 
65
65
  **You** approve specs and tasks. **The agent** runs gates and implements. **Gates** exit non-zero when paperwork or evidence is missing.
66
66
 
@@ -186,7 +186,7 @@ Full reference: **[Gates](docs/guide/gates.md)** · [Guarantees matrix](docs/gui
186
186
  | [Concepts](docs/guide/concepts.md) | Spec-driven + guardrails + loop + graph |
187
187
  | [Skills and hub](docs/guide/skills-and-hub.md) | What each skill file does |
188
188
  | [Gates](docs/guide/gates.md) | How each gate works |
189
- | [Platform parity](docs/guide/Platform-parity.md) | Shipped adapters (Cursor, Claude Code) — core works with any agent |
189
+ | [Platform parity](docs/guide/Platform-parity.md) | Shipped adapters (Cursor, Claude, Copilot, Codex) — core works with any agent |
190
190
  | [FAQ](docs/guide/FAQ.md) | Common questions |
191
191
  | [Changelog](docs/CHANGELOG.md) | Full version history |
192
192
 
@@ -202,7 +202,7 @@ npx @luizsantiago/spec-guardrails install
202
202
 
203
203
  | Version | What you gain |
204
204
  | --- | --- |
205
- | **3.1.x** | Doctor STATE fix; `validate-traceability` / `validate-quick`; `classify-change` / `feature-status`; Claude `CLAUDE.md` |
205
+ | **3.1.x** | Copilot/Codex/AGENTS.md adapters; doctor Process + Brakes scores; unified execution contracts; `validate-traceability` / `validate-quick`; `classify-change` / `feature-status` |
206
206
  | **3.0.x** | Final name Spec Guardrails; `.specs/guardrails/`; no dual-path ([Migration](docs/guide/Migration.md)) |
207
207
  | **2.2.x** | Seatbelt-era paths & markers; `doctor` Execute hints; docs split from README |
208
208
  | **2.1.x** | `loop-plan` + parallel `/loop` waves |
@@ -0,0 +1,17 @@
1
+ import { injectAgentsMd } from "./agents-md.js";
2
+ import { injectCodexAgents } from "./codex-agents.js";
3
+ import { injectCopilotInstructions } from "./copilot-instructions.js";
4
+
5
+ /**
6
+ * Install shipped platform adapter entry files (Copilot, Codex, AGENTS.md).
7
+ * Cursor and Claude adapters are injected separately in install.js.
8
+ *
9
+ * @param {string} cwd
10
+ */
11
+ export async function installPlatformAdapters(cwd) {
12
+ return Promise.all([
13
+ injectCopilotInstructions(cwd),
14
+ injectAgentsMd(cwd),
15
+ injectCodexAgents(cwd),
16
+ ]);
17
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ CURSORRULES_MARKER_BEGIN,
3
+ CURSORRULES_MARKER_END,
4
+ } from "./constants.js";
5
+
6
+ /**
7
+ * Build a Spec Guardrails execution contract block for a platform adapter.
8
+ *
9
+ * @param {{ skillPrefix: string, footerLines?: string[] }} options
10
+ * @returns {string}
11
+ */
12
+ export function buildExecutionContractBlock({ skillPrefix, footerLines = [] }) {
13
+ const footer =
14
+ footerLines.length > 0 ? `\n${footerLines.join("\n")}\n` : "\n";
15
+
16
+ return `${CURSORRULES_MARKER_BEGIN}
17
+ # Execution Contract (Spec Guardrails)
18
+
19
+ When planning architecture, specs, or multi-step features, read the hub first:
20
+
21
+ - \`${skillPrefix}/agent-architecture.md\` — SDD hub: contract, phases, gates, complexity router
22
+ - \`${skillPrefix}/references/\` — phase procedures (explore, project-init, constitution, specify, discuss, design, tasks, analyze, implement, validate, converge, archive, memory, quick-mode, context-limits, lessons, sub-agents)
23
+ - \`${skillPrefix}/task-graph-engineering.md\` — task DAG, parallelism, verify topology
24
+ - \`${skillPrefix}/engineering-standards.md\` — secure coding, code quality, artifact language
25
+ - \`${skillPrefix}/security-review.md\` — security checklist for /verify
26
+ - Sister skills (\`appsec\`, \`qa-strategy\`, \`code-simplify\`, \`ship-ready\`, \`git-handoff\`) — load **one conditional** at a time
27
+
28
+ Deterministic gates (\`python3\`, non-zero exit means STOP):
29
+
30
+ - Scripts in \`.specs/guardrails/scripts/\` — the **agent** runs them at phase boundaries (see hub).
31
+ - Humans: \`install\` once; optional \`feature-init\`, \`project-init\`, \`doctor\`, \`classify-change\`, \`feature-status\`.
32
+ - Full CLI: \`npx @luizsantiago/spec-guardrails --help\`
33
+ - Onboarding: \`.specs/GETTING_STARTED.md\`
34
+
35
+ All project artifacts are written in English.
36
+ Persistent state: \`.specs/STATE.md\`, \`.specs/lessons.json\`, \`.specs/LESSONS.md\`.${footer}${CURSORRULES_MARKER_END}
37
+ `;
38
+ }
39
+
40
+ /** @returns {string} Core contract body without adapter-specific footers (for parity tests). */
41
+ export function extractContractCore(block) {
42
+ const begin = block.indexOf(CURSORRULES_MARKER_BEGIN);
43
+ const end = block.indexOf(CURSORRULES_MARKER_END);
44
+ if (begin === -1 || end === -1 || end < begin) {
45
+ return block.trim();
46
+ }
47
+ let core = block.slice(begin + CURSORRULES_MARKER_BEGIN.length, end).trim();
48
+ const footerMarkers = [
49
+ "Project rules:",
50
+ "Cursor users also get",
51
+ "GitHub Copilot reads",
52
+ "OpenAI Codex adapter",
53
+ "Agent-agnostic entry",
54
+ ];
55
+ for (const marker of footerMarkers) {
56
+ const idx = core.indexOf(marker);
57
+ if (idx !== -1) {
58
+ core = core.slice(0, idx).trimEnd();
59
+ }
60
+ }
61
+ return core;
62
+ }
63
+
64
+ export const CURSORRULES_BLOCK = buildExecutionContractBlock({
65
+ skillPrefix: ".cursor/skills",
66
+ footerLines: ["Project rules: `.cursor/rules/engineering-baseline.mdc`"],
67
+ });
68
+
69
+ export const CLAUDE_MD_BLOCK = buildExecutionContractBlock({
70
+ skillPrefix: ".claude/skills",
71
+ footerLines: [
72
+ "Cursor users also get `.cursorrules` + `.cursor/rules/engineering-baseline.mdc` — same contract, different entrypoint. See Platform-parity docs in the package repo.",
73
+ ],
74
+ });
@@ -0,0 +1,21 @@
1
+ import { buildExecutionContractBlock } from "./agent-contract.js";
2
+ import { injectMarkedBlock } from "./marked-inject.js";
3
+
4
+ export const AGENTS_MD_BLOCK = buildExecutionContractBlock({
5
+ skillPrefix: ".github/skills",
6
+ footerLines: [
7
+ "Agent-agnostic entry (`AGENTS.md` open standard). Prefer the skills tree your tool loads:",
8
+ "- GitHub Copilot → `.github/skills/`",
9
+ "- OpenAI Codex → `.codex/skills/` (see `.codex/AGENTS.md`)",
10
+ "- Cursor → `.cursor/skills/` | Claude Code → `.claude/skills/`",
11
+ ],
12
+ });
13
+
14
+ /**
15
+ * Install or refresh root `AGENTS.md` for Codex and other agents that read it.
16
+ *
17
+ * @param {string} cwd
18
+ */
19
+ export async function injectAgentsMd(cwd) {
20
+ return injectMarkedBlock(cwd, "AGENTS.md", AGENTS_MD_BLOCK);
21
+ }
package/lib/claude-md.js CHANGED
@@ -1,63 +1,14 @@
1
- import path from "node:path";
2
-
1
+ import { CLAUDE_MD_BLOCK } from "./agent-contract.js";
2
+ import { injectMarkedBlock } from "./marked-inject.js";
3
3
  import {
4
4
  CURSORRULES_MARKER_BEGIN,
5
5
  CURSORRULES_MARKER_END,
6
- LEGACY_CURSORRULES_MARKER_PAIRS,
7
6
  } from "./constants.js";
8
- import {
9
- appendFileSafe,
10
- readFileSafe,
11
- writeFileSafe,
12
- } from "./fs-utils.js";
13
7
 
14
8
  export const CLAUDE_MD_MARKER_BEGIN = CURSORRULES_MARKER_BEGIN;
15
9
  export const CLAUDE_MD_MARKER_END = CURSORRULES_MARKER_END;
16
10
 
17
- export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER_BEGIN}
18
- # Execution Contract (Spec Guardrails)
19
-
20
- When planning architecture, specs, or multi-step features, read the hub first:
21
-
22
- - \`.claude/skills/agent-architecture.md\` — SDD hub: contract, phases, gates, complexity router
23
- - \`.claude/skills/references/\` — phase procedures (explore, project-init, constitution, specify, discuss, design, tasks, analyze, implement, validate, converge, archive, memory, quick-mode, context-limits, lessons, sub-agents)
24
- - \`.claude/skills/task-graph-engineering.md\` — task DAG, parallelism, verify topology
25
- - \`.claude/skills/engineering-standards.md\` — secure coding, code quality, artifact language
26
- - \`.claude/skills/security-review.md\` — security checklist for /verify
27
- - Sister skills (\`appsec\`, \`qa-strategy\`, \`code-simplify\`, \`ship-ready\`, \`git-handoff\`) — load **one conditional** at a time
28
-
29
- Deterministic gates (\`python3\`, non-zero exit means STOP):
30
-
31
- - Scripts in \`.specs/guardrails/scripts/\` — the **agent** runs them at phase boundaries (see hub).
32
- - Humans: \`install\` once; optional \`feature-init\`, \`project-init\`, \`doctor\`, \`classify-change\`, \`feature-status\`.
33
- - Full CLI: \`npx @luizsantiago/spec-guardrails --help\`
34
- - Onboarding: \`.specs/GETTING_STARTED.md\`
35
-
36
- All project artifacts are written in English.
37
- Persistent state: \`.specs/STATE.md\`, \`.specs/lessons.json\`, \`.specs/LESSONS.md\`.
38
-
39
- Cursor users also get \`.cursorrules\` + \`.cursor/rules/engineering-baseline.mdc\` — same contract, different entrypoint. See \`docs/guide/Platform-parity.md\` in the package repo.
40
- ${CLAUDE_MD_MARKER_END}
41
- `;
42
-
43
- const MARKER_PAIRS = [
44
- [CLAUDE_MD_MARKER_BEGIN, CLAUDE_MD_MARKER_END],
45
- ...LEGACY_CURSORRULES_MARKER_PAIRS,
46
- ];
47
-
48
- /**
49
- * @param {string} content
50
- */
51
- function locateBlock(content) {
52
- for (const [begin, endMarker] of MARKER_PAIRS) {
53
- const start = content.indexOf(begin);
54
- const end = content.indexOf(endMarker);
55
- if (start !== -1 && end !== -1 && end >= start) {
56
- return { start, end, endMarker };
57
- }
58
- }
59
- return null;
60
- }
11
+ export { CLAUDE_MD_BLOCK };
61
12
 
62
13
  /**
63
14
  * Install or refresh `.claude/CLAUDE.md` with the Spec Guardrails contract.
@@ -65,39 +16,5 @@ function locateBlock(content) {
65
16
  * @param {string} cwd
66
17
  */
67
18
  export async function injectClaudeMd(cwd) {
68
- const target = path.join(cwd, ".claude", "CLAUDE.md");
69
- const expected = CLAUDE_MD_BLOCK.trim();
70
-
71
- try {
72
- const existing = await readFileSafe(target);
73
- const located = locateBlock(existing);
74
- if (located) {
75
- const current = existing.slice(
76
- located.start,
77
- located.end + located.endMarker.length,
78
- );
79
- if (current.trim() === expected) {
80
- return { created: false, updated: false };
81
- }
82
- const before = existing.slice(0, located.start);
83
- const after = existing.slice(located.end + located.endMarker.length);
84
- const replaced = `${before}${expected}\n${after.replace(/^\n+/, "")}`;
85
- await writeFileSafe(
86
- target,
87
- replaced.endsWith("\n") ? replaced : `${replaced}\n`,
88
- );
89
- return { created: false, updated: true };
90
- }
91
-
92
- const separator = existing.endsWith("\n") ? "\n" : "\n\n";
93
- await appendFileSafe(target, `${separator}${CLAUDE_MD_BLOCK}\n`);
94
- return { created: false, updated: true };
95
- } catch (err) {
96
- if (err.code !== "ENOENT") {
97
- throw err;
98
- }
99
- }
100
-
101
- await writeFileSafe(target, `${CLAUDE_MD_BLOCK}\n`);
102
- return { created: true, updated: false };
19
+ return injectMarkedBlock(cwd, ".claude/CLAUDE.md", CLAUDE_MD_BLOCK);
103
20
  }
@@ -0,0 +1,19 @@
1
+ import { buildExecutionContractBlock } from "./agent-contract.js";
2
+ import { injectMarkedBlock } from "./marked-inject.js";
3
+
4
+ export const CODEX_AGENTS_BLOCK = buildExecutionContractBlock({
5
+ skillPrefix: ".codex/skills",
6
+ footerLines: [
7
+ "OpenAI Codex adapter — skills under `.codex/skills/`.",
8
+ "Root `AGENTS.md` and other platform files may also be present — use the tree your Codex session loads.",
9
+ ],
10
+ });
11
+
12
+ /**
13
+ * Install or refresh `.codex/AGENTS.md` for OpenAI Codex.
14
+ *
15
+ * @param {string} cwd
16
+ */
17
+ export async function injectCodexAgents(cwd) {
18
+ return injectMarkedBlock(cwd, ".codex/AGENTS.md", CODEX_AGENTS_BLOCK);
19
+ }
package/lib/constants.js CHANGED
@@ -20,7 +20,12 @@ export const REPO_RAW_URL = `${REPO_RAW_BASE}/${PINNED_REF}`;
20
20
 
21
21
  export const FALLBACK_REPO_URL = `${REPO_RAW_BASE}/${FALLBACK_REF}`;
22
22
 
23
- export const SKILL_DIRS = [".cursor/skills", ".claude/skills"];
23
+ export const SKILL_DIRS = [
24
+ ".cursor/skills",
25
+ ".claude/skills",
26
+ ".github/skills",
27
+ ".codex/skills",
28
+ ];
24
29
 
25
30
  export const CURSOR_RULES_DIR = ".cursor/rules";
26
31
 
@@ -160,31 +165,6 @@ export const LEGACY_CURSORRULES_MARKER_BEGIN = "<!-- AGENTIC-HARNESS:BEGIN -->";
160
165
  /** @deprecated Use LEGACY_CURSORRULES_MARKER_PAIRS */
161
166
  export const LEGACY_CURSORRULES_MARKER_END = "<!-- AGENTIC-HARNESS:END -->";
162
167
 
163
- export const CURSORRULES_BLOCK = `${CURSORRULES_MARKER_BEGIN}
164
- # Execution Contract (Spec Guardrails)
165
- When planning architecture, specs, or multi-step features, read the hub first:
166
- - \`.cursor/skills/agent-architecture.md\` — SDD hub: contract, phases, gates, complexity router
167
- - \`.cursor/skills/references/\` — phase procedures (explore, project-init, constitution, specify, discuss, design, tasks, analyze, implement, validate, converge, archive, memory, quick-mode, context-limits, lessons, sub-agents)
168
- - \`.cursor/skills/task-graph-engineering.md\` — task DAG, parallelism, verify topology
169
- - \`.cursor/skills/engineering-standards.md\` — secure coding, code quality, artifact language
170
- - \`.cursor/skills/security-review.md\` — security checklist for /verify
171
- - \`.cursor/skills/appsec.md\` — conditional AppSec (Complex / attack surface); never with other conditionals at once
172
- - \`.cursor/skills/qa-strategy.md\` — conditional QA strategy; load after AppSec if both apply
173
- - \`.cursor/skills/code-simplify.md\` — conditional simplify (Medium+ after A–D or owner ask)
174
- - \`.cursor/skills/ship-ready.md\` — conditional ship checklist (owner ask; does not authorize push)
175
- - \`.cursor/skills/git-handoff.md\` — git sync and session handoff for .specs/
176
-
177
- Deterministic gates (python3, non-zero exit means STOP):
178
- - Scripts in \`.specs/guardrails/scripts/\` — the **agent** runs them at phase boundaries (see hub).
179
- - Humans: \`install\` once; optional \`feature-init\`, \`project-init\`, \`doctor\`. Full CLI: \`npx @luizsantiago/spec-guardrails --help\`.
180
- - Onboarding: \`.specs/GETTING_STARTED.md\`
181
-
182
- All project artifacts are written in English.
183
- Persistent state: \`.specs/STATE.md\` (decisions/handoff), \`.specs/lessons.json\` (canonical lessons), and \`.specs/LESSONS.md\` (generated playbook).
184
- Project rules: \`.cursor/rules/engineering-baseline.mdc\`
185
- ${CURSORRULES_MARKER_END}
186
- `;
187
-
188
168
  const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
189
169
 
190
170
  /**
@@ -0,0 +1,23 @@
1
+ import { buildExecutionContractBlock } from "./agent-contract.js";
2
+ import { injectMarkedBlock } from "./marked-inject.js";
3
+
4
+ export const COPILOT_INSTRUCTIONS_BLOCK = buildExecutionContractBlock({
5
+ skillPrefix: ".github/skills",
6
+ footerLines: [
7
+ "GitHub Copilot reads this file as repository custom instructions.",
8
+ "Cursor and Claude Code use their own adapter entry files — see Platform-parity.md in the package repo.",
9
+ ],
10
+ });
11
+
12
+ /**
13
+ * Install or refresh `.github/copilot-instructions.md` for GitHub Copilot.
14
+ *
15
+ * @param {string} cwd
16
+ */
17
+ export async function injectCopilotInstructions(cwd) {
18
+ return injectMarkedBlock(
19
+ cwd,
20
+ ".github/copilot-instructions.md",
21
+ COPILOT_INSTRUCTIONS_BLOCK,
22
+ );
23
+ }
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
+ import { CURSORRULES_BLOCK } from "./agent-contract.js";
3
4
  import {
4
- CURSORRULES_BLOCK,
5
5
  CURSORRULES_MARKER_BEGIN,
6
6
  CURSORRULES_MARKER_END,
7
7
  LEGACY_CURSORRULES_MARKER_PAIRS,
package/lib/doctor.js CHANGED
@@ -3,13 +3,60 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { promisify } from "node:util";
5
5
 
6
- import { NPX, SKILL_DIRS } from "./constants.js";
6
+ import {
7
+ CURSORRULES_MARKER_BEGIN,
8
+ NPX,
9
+ SKILL_DIRS,
10
+ } from "./constants.js";
7
11
  import { resolvePython, resolveScriptsDir } from "./gates.js";
8
12
  import { readFileSafe } from "./fs-utils.js";
9
13
  import { listFeatureIds, readActiveFeatureFromState } from "./specs-utils.js";
10
14
 
11
15
  const execFileAsync = promisify(execFile);
12
16
 
17
+ /** @type {readonly string[]} */
18
+ export const DOCTOR_PROCESS_CHECK_IDS = [
19
+ "skills-hub",
20
+ "specs-scaffold",
21
+ "config",
22
+ "baseline-rule",
23
+ "state-feature",
24
+ "platform-adapters",
25
+ ];
26
+
27
+ /** @type {readonly string[]} */
28
+ export const DOCTOR_BRAKES_CHECK_IDS = [
29
+ "gate-scripts",
30
+ "python",
31
+ "gate-smoke",
32
+ ];
33
+
34
+ const ADAPTER_CONTRACT_PATHS = [
35
+ ".cursorrules",
36
+ ".claude/CLAUDE.md",
37
+ ".github/copilot-instructions.md",
38
+ "AGENTS.md",
39
+ ".codex/AGENTS.md",
40
+ ];
41
+
42
+ /**
43
+ * @param {string} cwd
44
+ * @returns {Promise<boolean>}
45
+ */
46
+ async function hasPlatformAdapterContract(cwd) {
47
+ for (const relativePath of ADAPTER_CONTRACT_PATHS) {
48
+ try {
49
+ const content = await readFileSafe(path.join(cwd, relativePath));
50
+ if (!content.includes(CURSORRULES_MARKER_BEGIN)) {
51
+ return false;
52
+ }
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+ return true;
58
+ }
59
+
13
60
  /**
14
61
  * @typedef {{
15
62
  * id: string,
@@ -61,18 +108,20 @@ export async function runDoctorChecks(cwd) {
61
108
  /** @type {DoctorCheck[]} */
62
109
  const checks = [];
63
110
 
64
- const hubInstalled = (
65
- await Promise.all(
66
- SKILL_DIRS.map((dir) => pathExists(cwd, path.join(dir, "agent-architecture.md"))),
67
- )
68
- ).some(Boolean);
111
+ const hubResults = await Promise.all(
112
+ SKILL_DIRS.map((dir) => pathExists(cwd, path.join(dir, "agent-architecture.md"))),
113
+ );
114
+ const hubInstalled = hubResults.every(Boolean);
115
+ const missingHubDirs = SKILL_DIRS.filter((_dir, index) => !hubResults[index]);
69
116
 
70
117
  checks.push({
71
118
  id: "skills-hub",
72
- label: "Agent hub skill (agent-architecture.md)",
119
+ label: "Agent hub skill in all adapter trees (agent-architecture.md)",
73
120
  weight: 12,
74
121
  pass: hubInstalled,
75
- suggest: NPX("install"),
122
+ suggest: hubInstalled
123
+ ? undefined
124
+ : `${NPX("install")} — missing hub under: ${missingHubDirs.join(", ")}`,
76
125
  });
77
126
 
78
127
  const scriptsDir = await resolveScriptsDir(cwd);
@@ -131,6 +180,14 @@ export async function runDoctorChecks(cwd) {
131
180
  suggest: NPX("install"),
132
181
  });
133
182
 
183
+ checks.push({
184
+ id: "platform-adapters",
185
+ label: "Platform adapter contracts (Cursor, Claude, Copilot, Codex, AGENTS.md)",
186
+ weight: 5,
187
+ pass: await hasPlatformAdapterContract(cwd),
188
+ suggest: NPX("install"),
189
+ });
190
+
134
191
  const hasProjectMd = await pathExists(cwd, ".specs/project/PROJECT.md");
135
192
  checks.push({
136
193
  id: "project-context",
@@ -165,14 +222,21 @@ export async function runDoctorChecks(cwd) {
165
222
  if (gatesPresent && pythonOk) {
166
223
  try {
167
224
  const script = path.join(cwd, scriptsDir, "check_commit.py");
168
- const python = await resolvePython();
169
- if (python) {
170
- await execFileAsync(python, [
171
- script,
172
- "--message",
173
- "chore(guardrails): doctor smoke test",
174
- ], { cwd });
175
- gateSmoke = true;
225
+ const hasCommon = await pathExists(cwd, path.join(scriptsDir, "_common.py"));
226
+ const hasCheckCommit = await pathExists(
227
+ cwd,
228
+ path.join(scriptsDir, "check_commit.py"),
229
+ );
230
+ if (hasCommon && hasCheckCommit) {
231
+ const python = await resolvePython();
232
+ if (python) {
233
+ await execFileAsync(
234
+ python.command,
235
+ [...python.args, script, "--message", "chore(guardrails): doctor smoke test"],
236
+ { cwd },
237
+ );
238
+ gateSmoke = true;
239
+ }
176
240
  }
177
241
  } catch {
178
242
  gateSmoke = false;
@@ -256,8 +320,22 @@ export async function resolveExecuteHint(cwd, activeFeature) {
256
320
  * @returns {number}
257
321
  */
258
322
  export function scoreDoctorChecks(checks) {
259
- const scored = checks.filter((check) => !check.optional);
260
- const earned = scored.filter((check) => check.pass).reduce((sum, check) => sum + check.weight, 0);
323
+ return scoreDoctorChecksForIds(checks, null);
324
+ }
325
+
326
+ /**
327
+ * @param {DoctorCheck[]} checks
328
+ * @param {readonly string[] | null} ids when null, score all non-optional checks
329
+ * @returns {number}
330
+ */
331
+ export function scoreDoctorChecksForIds(checks, ids) {
332
+ const idSet = ids ? new Set(ids) : null;
333
+ const scored = checks.filter(
334
+ (check) => !check.optional && (idSet === null || idSet.has(check.id)),
335
+ );
336
+ const earned = scored
337
+ .filter((check) => check.pass)
338
+ .reduce((sum, check) => sum + check.weight, 0);
261
339
  const total = scored.reduce((sum, check) => sum + check.weight, 0);
262
340
  if (total === 0) {
263
341
  return 0;
@@ -265,6 +343,19 @@ export function scoreDoctorChecks(checks) {
265
343
  return Math.round((earned / total) * 100);
266
344
  }
267
345
 
346
+ /**
347
+ * @param {DoctorCheck[]} checks
348
+ * @returns {{ process: { score: number, ready: boolean }, brakes: { score: number, ready: boolean } }}
349
+ */
350
+ export function scoreDoctorModes(checks) {
351
+ const processScore = scoreDoctorChecksForIds(checks, DOCTOR_PROCESS_CHECK_IDS);
352
+ const brakesScore = scoreDoctorChecksForIds(checks, DOCTOR_BRAKES_CHECK_IDS);
353
+ return {
354
+ process: { score: processScore, ready: processScore >= 80 },
355
+ brakes: { score: brakesScore, ready: brakesScore >= 80 },
356
+ };
357
+ }
358
+
268
359
  /**
269
360
  * @param {DoctorCheck[]} checks
270
361
  * @param {number} limit
@@ -277,11 +368,12 @@ export function topDoctorSuggestions(checks, limit = 3) {
277
368
  /**
278
369
  * @param {string} cwd
279
370
  * @param {{ suggest?: boolean, json?: boolean }} [options]
280
- * @returns {Promise<{ score: number, checks: DoctorCheck[], suggestions: DoctorCheck[], executeHint: string | null }>}
371
+ * @returns {Promise<{ score: number, modes: ReturnType<typeof scoreDoctorModes>, checks: DoctorCheck[], suggestions: DoctorCheck[], executeHint: string | null, pythonMissing: boolean }>}
281
372
  */
282
373
  export async function doctor(cwd, options = {}) {
283
374
  const checks = await runDoctorChecks(cwd);
284
375
  const score = scoreDoctorChecks(checks);
376
+ const modes = scoreDoctorModes(checks);
285
377
  const suggestions = topDoctorSuggestions(checks);
286
378
  const activeFeature = await readActiveFeature(cwd);
287
379
  const executeHint = await resolveExecuteHint(cwd, activeFeature);
@@ -291,22 +383,28 @@ export async function doctor(cwd, options = {}) {
291
383
  if (options.json) {
292
384
  console.log(
293
385
  JSON.stringify(
294
- { score, checks, suggestions, executeHint, pythonMissing },
386
+ { score, modes, checks, suggestions, executeHint, pythonMissing },
295
387
  null,
296
388
  2,
297
389
  ),
298
390
  );
299
- return { score, checks, suggestions, executeHint, pythonMissing };
391
+ return { score, modes, checks, suggestions, executeHint, pythonMissing };
300
392
  }
301
393
 
302
394
  if (pythonMissing) {
303
395
  console.log(
304
- "⚠ PYTHON MISSINGstructural gates will not run automatically.\n" +
305
- " Install Python 3.10+ (python3 or python on PATH), then re-run doctor.\n" +
306
- " Until then the agent must perform the same checklists manually from skills/references/.\n",
396
+ "⚠ BRAKES OFFPython 3.10+ not found.\n" +
397
+ " Process mode works (workflow + .specs/ + manual checklists from skills/references/).\n" +
398
+ " Install Python 3.10+ (python3 or python on PATH), then re-run doctor for Brakes mode.\n",
307
399
  );
308
400
  }
309
401
 
402
+ const brakesHint = modes.brakes.ready
403
+ ? ""
404
+ : " — install Python 3.10+ and gate scripts for automatic enforcement";
405
+ console.log("Operating modes");
406
+ console.log(` Process: ${modes.process.score}/100`);
407
+ console.log(` Brakes: ${modes.brakes.score}/100${brakesHint}\n`);
310
408
  console.log(`Guardrails Ready: ${score}/100\n`);
311
409
 
312
410
  for (const check of checks) {
@@ -329,5 +427,5 @@ export async function doctor(cwd, options = {}) {
329
427
  console.log(`\nExecute hint:\n → ${executeHint}`);
330
428
  }
331
429
 
332
- return { score, checks, suggestions, executeHint, pythonMissing };
430
+ return { score, modes, checks, suggestions, executeHint, pythonMissing };
333
431
  }
package/lib/gates.js CHANGED
@@ -5,7 +5,15 @@ import path from "node:path";
5
5
 
6
6
  import { NPX, GUARDRAILS_SCRIPTS_DIR } from "./constants.js";
7
7
 
8
- const PYTHON_CANDIDATES = ["python3", "python"];
8
+ /** @typedef {{ command: string, args: string[] }} PythonInterpreter */
9
+
10
+ const PYTHON_CANDIDATES = [
11
+ { command: "python3", args: [] },
12
+ { command: "python", args: [] },
13
+ { command: "py", args: ["-3"] },
14
+ ];
15
+
16
+ const MIN_PYTHON = [3, 10];
9
17
 
10
18
  const GATE_SCRIPTS = {
11
19
  "validate-spec": "validate_spec.py",
@@ -44,6 +52,61 @@ function run(command, args, options = {}) {
44
52
  });
45
53
  }
46
54
 
55
+ /**
56
+ * @param {string} output
57
+ * @returns {[number, number] | null}
58
+ */
59
+ export function parsePythonVersion(output) {
60
+ const match = output.match(/(\d+)\.(\d+)/);
61
+ if (!match) {
62
+ return null;
63
+ }
64
+ return [Number(match[1]), Number(match[2])];
65
+ }
66
+
67
+ /**
68
+ * @param {[number, number] | null} version
69
+ * @param {[number, number]} minimum
70
+ * @returns {boolean}
71
+ */
72
+ export function satisfiesMinPython(version, minimum = MIN_PYTHON) {
73
+ if (!version) {
74
+ return false;
75
+ }
76
+ if (version[0] !== minimum[0]) {
77
+ return version[0] > minimum[0];
78
+ }
79
+ return version[1] >= minimum[1];
80
+ }
81
+
82
+ /**
83
+ * @param {PythonInterpreter} interpreter
84
+ * @returns {Promise<string>}
85
+ */
86
+ async function readPythonVersion(interpreter) {
87
+ let output = "";
88
+ await new Promise((resolve, reject) => {
89
+ const child = spawn(interpreter.command, [...interpreter.args, "--version"], {
90
+ stdio: ["ignore", "pipe", "pipe"],
91
+ });
92
+ child.stdout?.on("data", (chunk) => {
93
+ output += chunk.toString();
94
+ });
95
+ child.stderr?.on("data", (chunk) => {
96
+ output += chunk.toString();
97
+ });
98
+ child.on("error", reject);
99
+ child.on("close", (code) => {
100
+ if (code === 0) {
101
+ resolve(undefined);
102
+ } else {
103
+ reject(new Error(`${interpreter.command} --version exited ${code}`));
104
+ }
105
+ });
106
+ });
107
+ return output;
108
+ }
109
+
47
110
  /**
48
111
  * Resolve gate scripts directory (`.specs/guardrails/scripts` only — no legacy dual-path).
49
112
  *
@@ -55,18 +118,20 @@ export async function resolveScriptsDir(_cwd) {
55
118
  }
56
119
 
57
120
  /**
58
- * Resolve the available Python interpreter, or null when none is installed.
59
- * @returns {Promise<string | null>}
121
+ * Resolve a Python 3.10+ interpreter, or null when none qualifies.
122
+ *
123
+ * @returns {Promise<PythonInterpreter | null>}
60
124
  */
61
125
  export async function resolvePython() {
62
126
  for (const candidate of PYTHON_CANDIDATES) {
63
127
  try {
64
- const code = await run(candidate, ["--version"], { stdio: "ignore" });
65
- if (code === 0) {
128
+ const output = await readPythonVersion(candidate);
129
+ const version = parsePythonVersion(output);
130
+ if (satisfiesMinPython(version, MIN_PYTHON)) {
66
131
  return candidate;
67
132
  }
68
133
  } catch {
69
- // Interpreter not on PATH; try the next candidate.
134
+ // Interpreter not on PATH or below minimum; try the next candidate.
70
135
  }
71
136
  }
72
137
 
@@ -112,12 +177,12 @@ export async function runGuardrailsScript(command, args, options = {}) {
112
177
 
113
178
  if (!python) {
114
179
  throw new Error(
115
- "Python 3 not found. Install Python 3.10+ for Brakes mode (automatic gates), " +
180
+ "Python 3.10+ not found. Install Python 3.10+ for Brakes mode (automatic gates), " +
116
181
  "or perform the equivalent checks manually in Process mode.",
117
182
  );
118
183
  }
119
184
 
120
- return run(python, [scriptPath, ...args], {
185
+ return run(python.command, [...python.args, scriptPath, ...args], {
121
186
  cwd,
122
187
  stdio: options.stdio,
123
188
  });
package/lib/install.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  DISPLAY_NAME,
13
13
  resolveAssetOverride,
14
14
  } from "./constants.js";
15
+ import { installPlatformAdapters } from "./adapters.js";
15
16
  import { injectClaudeMd } from "./claude-md.js";
16
17
  import { injectCursorRules } from "./cursorrules.js";
17
18
  import { ensureDir, readFileSafe, writeFileIfMissing } from "./fs-utils.js";
@@ -115,8 +116,11 @@ export async function install(options = {}) {
115
116
  log("✅ LESSONS.md initialized [feedback loop]");
116
117
  }
117
118
 
119
+ log("🔗 Installing platform adapters...");
118
120
  await injectCursorRules(cwd);
119
121
  await injectClaudeMd(cwd);
122
+ await installPlatformAdapters(cwd);
123
+ log("✅ Adapters → .cursorrules, CLAUDE.md, copilot-instructions.md, AGENTS.md, .codex/AGENTS.md");
120
124
 
121
125
  const gettingStartedCreated = await writeFileIfMissing(
122
126
  path.join(cwd, ".specs/GETTING_STARTED.md"),
@@ -0,0 +1,78 @@
1
+ import path from "node:path";
2
+
3
+ import {
4
+ CURSORRULES_MARKER_BEGIN,
5
+ CURSORRULES_MARKER_END,
6
+ LEGACY_CURSORRULES_MARKER_PAIRS,
7
+ } from "./constants.js";
8
+ import {
9
+ appendFileSafe,
10
+ readFileSafe,
11
+ writeFileSafe,
12
+ } from "./fs-utils.js";
13
+
14
+ const MARKER_PAIRS = [
15
+ [CURSORRULES_MARKER_BEGIN, CURSORRULES_MARKER_END],
16
+ ...LEGACY_CURSORRULES_MARKER_PAIRS,
17
+ ];
18
+
19
+ /**
20
+ * @param {string} content
21
+ * @returns {{ start: number, end: number, endMarker: string } | null}
22
+ */
23
+ function locateBlock(content) {
24
+ for (const [begin, endMarker] of MARKER_PAIRS) {
25
+ const start = content.indexOf(begin);
26
+ const end = content.indexOf(endMarker);
27
+ if (start !== -1 && end !== -1 && end >= start) {
28
+ return { start, end, endMarker };
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Install or refresh a markdown file with a marked Spec Guardrails block.
36
+ *
37
+ * @param {string} cwd
38
+ * @param {string} relativePath
39
+ * @param {string} blockContent
40
+ * @returns {Promise<{ created: boolean, updated: boolean }>}
41
+ */
42
+ export async function injectMarkedBlock(cwd, relativePath, blockContent) {
43
+ const target = path.join(cwd, relativePath);
44
+ const expected = blockContent.trim();
45
+
46
+ try {
47
+ const existing = await readFileSafe(target);
48
+ const located = locateBlock(existing);
49
+ if (located) {
50
+ const current = existing.slice(
51
+ located.start,
52
+ located.end + located.endMarker.length,
53
+ );
54
+ if (current.trim() === expected) {
55
+ return { created: false, updated: false };
56
+ }
57
+ const before = existing.slice(0, located.start);
58
+ const after = existing.slice(located.end + located.endMarker.length);
59
+ const replaced = `${before}${expected}\n${after.replace(/^\n+/, "")}`;
60
+ await writeFileSafe(
61
+ target,
62
+ replaced.endsWith("\n") ? replaced : `${replaced}\n`,
63
+ );
64
+ return { created: false, updated: true };
65
+ }
66
+
67
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
68
+ await appendFileSafe(target, `${separator}${blockContent}\n`);
69
+ return { created: false, updated: true };
70
+ } catch (err) {
71
+ if (err.code !== "ENOENT") {
72
+ throw err;
73
+ }
74
+ }
75
+
76
+ await writeFileSafe(target, `${blockContent}\n`);
77
+ return { created: true, updated: false };
78
+ }
package/lib/next-steps.js CHANGED
@@ -2,6 +2,9 @@
2
2
  * Human-facing messages after install — keep CLI surface minimal.
3
3
  */
4
4
 
5
+ const DOCS_BASE =
6
+ "https://github.com/luizssantiago92/spec-guardrails/blob/main/docs/guide";
7
+
5
8
  /**
6
9
  * @param {{ pythonAvailable?: boolean, preset?: string }} [options]
7
10
  * @returns {string[]}
@@ -12,11 +15,11 @@ export function formatInstallNextSteps(options = {}) {
12
15
  "✨ Setup complete.",
13
16
  "",
14
17
  "Next:",
15
- " 1. Open your AI coding agent in this project (Cursor and Claude Code adapters install automatically).",
18
+ " 1. Open your AI coding agent in this project (Cursor, Claude, Copilot, Codex, or AGENTS.md adapters install automatically).",
16
19
  " 2. Run **Specify** (`/specify` or “Specify a feature: …”).",
17
20
  "",
18
- " Other agents: core + CLI work anywhere — see docs/guide/Architecture.md",
19
- " Guide: docs/guide/Quick-start.md (repo) · .specs/GETTING_STARTED.md (this project)",
21
+ ` Architecture: ${DOCS_BASE}/Architecture.md`,
22
+ ` Quick start: ${DOCS_BASE}/Quick-start.md · .specs/GETTING_STARTED.md (this project)`,
20
23
  ];
21
24
 
22
25
  if (options.preset) {
@@ -34,7 +37,7 @@ export function formatInstallNextSteps(options = {}) {
34
37
  "",
35
38
  "Optional CLI (you rarely need these on day one):",
36
39
  " project-init existing repo with code already",
37
- " doctor if something looks wrong",
40
+ " doctor Process + Brakes readiness scores",
38
41
  " --help full command list",
39
42
  "",
40
43
  );
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  import { CURSOR_RULES_DIR, RULE_ASSETS } from "./constants.js";
5
- import { ensureDir } from "./fs-utils.js";
5
+ import { assertSafeWriteTarget, ensureDir } from "./fs-utils.js";
6
6
 
7
7
  /** Markers around the catalog skills table — refreshed on every install. */
8
8
  export const SKILLS_MAP_START = "<!-- guardrails-managed:skills-map:start -->";
@@ -194,12 +194,14 @@ export async function installProjectRules(cwd, options) {
194
194
  }
195
195
 
196
196
  if (!exists) {
197
+ await assertSafeWriteTarget(destPath);
197
198
  await fs.rename(tmpPath, destPath);
198
199
  continue;
199
200
  }
200
201
 
201
202
  const existing = await fs.readFile(destPath, "utf8");
202
203
  const merged = mergeBaselineRule(existing, shipped);
204
+ await assertSafeWriteTarget(destPath);
203
205
  await fs.writeFile(destPath, merged, "utf8");
204
206
  await fs.rm(tmpPath, { force: true });
205
207
  } catch (err) {
@@ -36,7 +36,7 @@ export async function resolveFeatureId(raw, cwd) {
36
36
  const trimmed = raw.trim();
37
37
  const asPath = path.resolve(cwd, trimmed);
38
38
 
39
- if (trimmed.endsWith(".md") || trimmed.includes("/")) {
39
+ if (trimmed.endsWith(".md") || trimmed.includes("/") || trimmed.includes("\\")) {
40
40
  const relative = path.relative(path.join(cwd, FEATURES_DIR), asPath);
41
41
  if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
42
42
  return relative.split(path.sep)[0];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@luizsantiago/spec-guardrails",
3
- "version": "3.1.4",
4
- "description": "Spec-driven process kit for any repo-local AI agent: Process mode (Node) or full Brakes mode (Node + Python gates). Progressive loading, independent verify, Guarantees matrix.",
3
+ "version": "3.1.8",
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": {
7
7
  "spec-guardrails": "./index.js"
@@ -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",
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",
16
16
  "test:gates": "node test/run-gate-tests.mjs",
17
17
  "prepublishOnly": "npm test"
18
18
  },
@@ -14,7 +14,7 @@ This file is the contract and the map. Phase procedures live in `references/`; c
14
14
 
15
15
  ## Critical Rules (read before acting)
16
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.
17
+ **Reference files.** Phase procedures live in `references/` next to this file (`.cursor/skills/references/`, `.claude/skills/references/`, `.github/skills/references/`, `.codex/skills/references/` — use the tree your agent loads). 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
18
 
19
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
20
 
@@ -45,8 +45,11 @@ Structural gates run **before** owner review, so they cannot drift when the mode
45
45
  | Before confirming a spec | `python3 .specs/guardrails/scripts/validate_spec.py [feature]` |
46
46
  | Before approving tasks | `python3 .specs/guardrails/scripts/analyze_artifacts.py [feature]` |
47
47
  | Before presenting tasks for approval | `python3 .specs/guardrails/scripts/validate_tasks.py [feature]` |
48
+ | Before Execute waves (3+ tasks) | `npx @luizsantiago/spec-guardrails loop-plan [feature]` |
48
49
  | On each commit | `python3 .specs/guardrails/scripts/check_commit.py --message "<message>"` |
49
50
  | Before declaring a feature done | `python3 .specs/guardrails/scripts/validate_state.py [feature]` |
51
+ | Traceability (Medium+ features) | `python3 .specs/guardrails/scripts/validate_traceability.py [feature]` |
52
+ | Quick mode evidence | `python3 .specs/guardrails/scripts/validate_quick.py [feature]` |
50
53
  | After Verify PASS | `npx @luizsantiago/spec-guardrails archive-feature [feature]` (Tier 0) |
51
54
  | Before a phase procedure (optional) | `npx @luizsantiago/spec-guardrails phase-context <phase>` |
52
55
  | After a FAIL verdict | `python3 .specs/guardrails/scripts/lessons.py add --source .specs/features/[feature]/validation.md` |
@@ -55,7 +58,7 @@ Gates accept a feature name, a feature directory, or a path to the artifact. Wit
55
58
 
56
59
  A **non-zero exit means STOP** — fix the artifact, then re-run the gate. Never continue past a failing gate.
57
60
 
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.
61
+ **Process mode (Brakes off).** If Python 3.10+ or shell execution is unavailable, say so once, then perform the same checks by reading the artifact against the reference checklist. Process mode never lowers the standard; it only changes who runs the check. Run `doctor` to see separate **Process** and **Brakes** scores.
59
62
 
60
63
  ## Phase Map
61
64
 
@@ -4,7 +4,7 @@ You installed the **Spec Guardrails**. You do **not** need to memorize CLI comma
4
4
 
5
5
  ## What to do now
6
6
 
7
- 1. Open your **AI coding agent** in this project (Cursor or Claude Code if you use those adapters).
7
+ 1. Open your **AI coding agent** in this project (Cursor, Claude Code, GitHub Copilot, OpenAI Codex, or any agent that reads root `AGENTS.md`).
8
8
  2. Start with **Specify** (an **agent command** — chat, not terminal):
9
9
 
10
10
  ```