@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 +5 -5
- package/lib/adapters.js +17 -0
- package/lib/agent-contract.js +74 -0
- package/lib/agents-md.js +21 -0
- package/lib/claude-md.js +4 -87
- package/lib/codex-agents.js +19 -0
- package/lib/constants.js +6 -26
- package/lib/copilot-instructions.js +23 -0
- package/lib/cursorrules.js +1 -1
- package/lib/doctor.js +123 -25
- package/lib/gates.js +73 -8
- package/lib/install.js +4 -0
- package/lib/marked-inject.js +78 -0
- package/lib/next-steps.js +7 -4
- package/lib/project-rules.js +3 -1
- package/lib/specs-utils.js +1 -1
- package/package.json +3 -3
- package/skills/agent-architecture.md +5 -2
- package/templates/GETTING_STARTED.md +1 -1
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 (
|
|
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
|
|
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** |
|
|
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
|
|
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** |
|
|
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 |
|
package/lib/adapters.js
ADDED
|
@@ -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
|
+
});
|
package/lib/agents-md.js
ADDED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 = [
|
|
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
|
+
}
|
package/lib/cursorrules.js
CHANGED
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 {
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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:
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
"⚠
|
|
305
|
-
"
|
|
306
|
-
"
|
|
396
|
+
"⚠ BRAKES OFF — Python 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
|
-
|
|
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
|
|
59
|
-
*
|
|
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
|
|
65
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
40
|
+
" doctor Process + Brakes readiness scores",
|
|
38
41
|
" --help full command list",
|
|
39
42
|
"",
|
|
40
43
|
);
|
package/lib/project-rules.js
CHANGED
|
@@ -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) {
|
package/lib/specs-utils.js
CHANGED
|
@@ -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
|
-
"description": "
|
|
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
|
-
**
|
|
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
|
|
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
|
```
|