@luizsantiago/spec-guardrails 4.5.1 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/index.js +35 -4
- package/lib/agent-contract.js +1 -1
- package/lib/feature-overview.js +237 -0
- package/lib/gates.js +12 -0
- package/lib/install.js +1 -1
- package/package.json +2 -2
- package/scripts/_project_config.py +10 -10
- package/scripts/analyze_artifacts.py +2 -0
- package/scripts/check_commit.py +3 -3
- package/scripts/check_suppressions.py +3 -8
- package/scripts/run_quality_checks.py +2 -2
- package/skills/agent-architecture.md +6 -5
- package/skills/references/converge.md +6 -4
- package/skills/references/implement.md +6 -4
- package/skills/references/sub-agents.md +17 -0
- package/skills/security-review.md +16 -0
- package/skills/task-graph-engineering.md +1 -0
- package/templates/config.yaml.example +6 -1
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Spec Guardrails
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@luizsantiago/spec-guardrails)
|
|
4
|
-
[](https://www.npmjs.com/package/@luizsantiago/spec-guardrails)
|
|
5
4
|
[](https://github.com/luizssantiago92/spec-guardrails/actions/workflows/ci.yml)
|
|
6
5
|
[](LICENSE)
|
|
7
6
|
|
|
@@ -11,7 +10,7 @@
|
|
|
11
10
|
|
|
12
11
|
Spec Guardrails installs a working method into your repository: the agent writes down what it is going to build, gets your approval, implements in small waves, and proves the result before calling it done. Nothing about your stack changes — you get written requirements, a task plan, and verification evidence stored as files in the project.
|
|
13
12
|
|
|
14
|
-
npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.
|
|
13
|
+
npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.6.x**
|
|
15
14
|
|
|
16
15
|
---
|
|
17
16
|
|
|
@@ -39,7 +38,9 @@ npx @luizsantiago/spec-guardrails install
|
|
|
39
38
|
npx @luizsantiago/spec-guardrails doctor
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
`install` writes the phase guides for your agent and creates the `.specs/` folder.
|
|
41
|
+
`install` writes the phase guides for your agent and creates the `.specs/` folder. By default it detects your platform (Cursor, Claude Code, Copilot, or Codex) and installs **one** skill tree plus that platform's adapter entry file. Existing trees are preserved when you switch IDEs. Use `install --all-platforms` for every tree, or `install --platform <id>` to force one.
|
|
42
|
+
|
|
43
|
+
Re-run `install` after upgrading the package — your existing `.specs/` notes are preserved. After that, you work in **agent chat**, not in the terminal; the agent calls the CLI and checks when needed.
|
|
43
44
|
|
|
44
45
|
| Requirement | Role |
|
|
45
46
|
| --- | --- |
|
|
@@ -59,7 +60,7 @@ In short: Python turns "trust the agent" into "the agent has to prove it."
|
|
|
59
60
|
|
|
60
61
|
Which checks exist and what each one requires: [Gates](docs/guide/gates.md) · [Guarantees matrix](docs/guide/Guarantees-matrix.md)
|
|
61
62
|
|
|
62
|
-
Read more: [Quick start](docs/guide/Quick-start.md) · [Platform parity](docs/guide/Platform-parity.md) · [CHANGELOG](docs/CHANGELOG.md)
|
|
63
|
+
Read more: [Quick start](docs/guide/Quick-start.md) · [Tutorials](docs/guide/tutorials/README.md) · [Platform parity](docs/guide/Platform-parity.md) · [CHANGELOG](docs/CHANGELOG.md)
|
|
63
64
|
|
|
64
65
|
---
|
|
65
66
|
|
|
@@ -139,7 +140,7 @@ Automatic checks at step boundaries — each one blocks a specific kind of short
|
|
|
139
140
|
| `validate-traceability` | REQ → task → proof chain is broken |
|
|
140
141
|
| `validate-state` | Feature is declared done without evidence |
|
|
141
142
|
| `validate-quick` | Quick-mode fix broke its size or shape rules |
|
|
142
|
-
| `check-commit` | Commit message
|
|
143
|
+
| `check-commit` | Commit message is not conventional, staged diff is empty, or exceeds `commit.max_staged_lines` |
|
|
143
144
|
| `check-suppressions` | Staged diff adds `# noqa`, `eslint-disable`, `@ts-ignore`, skipped tests, or `--no-verify` |
|
|
144
145
|
| `quality-checks` | Configured project commands (`npm test`, …) fail during `/verify` |
|
|
145
146
|
| `lessons` | A failed verify tries to skip the lesson step |
|
|
@@ -235,7 +236,7 @@ Spec Guardrails adapts patterns from open-source work. These are the projects wh
|
|
|
235
236
|
| [addyosmani/agent-skills](https://github.com/addyosmani/agent-skills) | MIT | Design-discussion patterns and definition-of-done framing |
|
|
236
237
|
| [graph-engineering](https://github.com/codejunkie99/graph-engineering) | MIT | Task-graph rules behind safe parallel waves |
|
|
237
238
|
| [loop-engineering](https://github.com/cobusgreyling/loop-engineering) | MIT | Wave-based execution model |
|
|
238
|
-
| [loopgate_harness](https://github.com/rxdt/loopgate_harness) | MIT | Suppression-bypass blocking, project-configured quality commands as verify evidence, honest-limits and
|
|
239
|
+
| [loopgate_harness](https://github.com/rxdt/loopgate_harness) | MIT | Suppression-bypass blocking, project-configured quality commands as verify evidence, honest-limits framing, and README diagram layout (proxy-safe SVG; SMIL animation on GitHub) |
|
|
239
240
|
|
|
240
241
|
Everything else — the CLI, the Python checks, the platform adapters, and the requirements-analysis phase — is original work in this repository. Full lineage, including references we cite but do not bundle: [Credits and lineage](docs/guide/credits.md).
|
|
241
242
|
|
package/index.js
CHANGED
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
savePolicyState,
|
|
26
26
|
} from "./lib/execution-policy.js";
|
|
27
27
|
import { featureInit } from "./lib/feature.js";
|
|
28
|
+
import {
|
|
29
|
+
featureOverview,
|
|
30
|
+
formatFeatureOverview,
|
|
31
|
+
} from "./lib/feature-overview.js";
|
|
28
32
|
import { featureStatus, formatFeatureStatus } from "./lib/feature-status.js";
|
|
29
33
|
import { GATE_COMMANDS, AUX_COMMANDS, runGate, runGuardrailsScript } from "./lib/gates.js";
|
|
30
34
|
import { install } from "./lib/install.js";
|
|
@@ -101,10 +105,13 @@ Commands:
|
|
|
101
105
|
[--no-roadmap] Skip ROADMAP update
|
|
102
106
|
[--no-domain] Skip domain spec merge
|
|
103
107
|
[--no-state] Skip STATE reset
|
|
104
|
-
classify-change
|
|
108
|
+
classify-change <desc> [files...] Heuristic complexity tier (quick/simple/medium/complex)
|
|
105
109
|
[--json] Machine-readable output
|
|
106
110
|
feature-status [feature] Artifact checklist + next step for a feature
|
|
107
111
|
[--json] Machine-readable output
|
|
112
|
+
feature-overview [feature] REQ → task → evidence dashboard (markdown)
|
|
113
|
+
[--write] Save .specs/features/<feature>/overview.md
|
|
114
|
+
[--json] Machine-readable output (no markdown body)
|
|
108
115
|
phase-context <phase> Print .specs/config.yaml context + rules for a phase
|
|
109
116
|
doctor [path] Audit guardrails readiness (score + next actions)
|
|
110
117
|
[--json] Machine-readable output
|
|
@@ -872,9 +879,6 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
872
879
|
process.stdout.write(formatExplorationStatus(status, { json }));
|
|
873
880
|
} else if (sub === "validate") {
|
|
874
881
|
const featureId = rest[0];
|
|
875
|
-
if (!featureId) {
|
|
876
|
-
throw new Error("Usage: solution-explore validate <feature>");
|
|
877
|
-
}
|
|
878
882
|
const result = await validateExplorationArtifact(cwd, featureId);
|
|
879
883
|
process.stdout.write(formatExplorationValidation(result, { json }));
|
|
880
884
|
if (!result.ok) {
|
|
@@ -1082,6 +1086,33 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
1082
1086
|
console.error(`❌ ${err.message}`);
|
|
1083
1087
|
process.exit(1);
|
|
1084
1088
|
}
|
|
1089
|
+
} else if (command === "feature-overview") {
|
|
1090
|
+
try {
|
|
1091
|
+
let json = false;
|
|
1092
|
+
let write = false;
|
|
1093
|
+
const positional = [];
|
|
1094
|
+
for (const arg of args) {
|
|
1095
|
+
if (arg === "--json") {
|
|
1096
|
+
json = true;
|
|
1097
|
+
} else if (arg === "--write") {
|
|
1098
|
+
write = true;
|
|
1099
|
+
} else {
|
|
1100
|
+
positional.push(arg);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const overview = await featureOverview(positional[0], { write });
|
|
1104
|
+
if (json) {
|
|
1105
|
+
console.log(JSON.stringify(overview, null, 2));
|
|
1106
|
+
} else {
|
|
1107
|
+
process.stdout.write(formatFeatureOverview(overview));
|
|
1108
|
+
if (overview.writtenTo) {
|
|
1109
|
+
process.stderr.write(`\nWrote ${overview.writtenTo}\n`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
console.error(`❌ ${err.message}`);
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1085
1116
|
} else if (AUX_COMMANDS.includes(command)) {
|
|
1086
1117
|
try {
|
|
1087
1118
|
const code = await runGuardrailsScript(command, args);
|
package/lib/agent-contract.js
CHANGED
|
@@ -28,7 +28,7 @@ When planning architecture, specs, or multi-step features, read the hub first:
|
|
|
28
28
|
Deterministic gates (\`python3\`, non-zero exit means STOP):
|
|
29
29
|
|
|
30
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\`.
|
|
31
|
+
- Humans: \`install\` once; optional \`feature-init\`, \`project-init\`, \`doctor\`, \`classify-change\`, \`feature-status\`, \`feature-overview\`.
|
|
32
32
|
- Full CLI: \`npx @luizsantiago/spec-guardrails --help\`
|
|
33
33
|
- Onboarding: \`.specs/GETTING_STARTED.md\`
|
|
34
34
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { readFileSafe } from "./fs-utils.js";
|
|
5
|
+
import { featureStatus } from "./feature-status.js";
|
|
6
|
+
import { featureDir, resolveFeatureId } from "./specs-utils.js";
|
|
7
|
+
|
|
8
|
+
const REQ_ID = /\b[A-Z][A-Z0-9]{1,9}-\d{2,4}\b/g;
|
|
9
|
+
const TASK_HEADING = /^#{2,6}\s*(T\d+)[:\s]+(.+)$/gim;
|
|
10
|
+
const TASK_FIELD =
|
|
11
|
+
/^\s*[-*]?\s*\*{0,2}([A-Za-z][A-Za-z ]+?)\*{0,2}\s*:\s*(.+?)\s*$/gim;
|
|
12
|
+
const EVIDENCE = /[\w./\\-]+\.[A-Za-z][A-Za-z0-9]{0,9}:\d{1,6}\b/g;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} text
|
|
16
|
+
* @returns {string[]}
|
|
17
|
+
*/
|
|
18
|
+
function uniqueReqIds(text) {
|
|
19
|
+
return [...new Set(text.match(REQ_ID) ?? [])];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} tasksText
|
|
24
|
+
* @returns {Array<{ id: string, title: string, requirement: string | null, complete: boolean }>}
|
|
25
|
+
*/
|
|
26
|
+
function parseTasks(tasksText) {
|
|
27
|
+
const headings = [...tasksText.matchAll(TASK_HEADING)];
|
|
28
|
+
/** @type {Array<{ id: string, title: string, requirement: string | null, complete: boolean }>} */
|
|
29
|
+
const tasks = [];
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < headings.length; i += 1) {
|
|
32
|
+
const start = headings[i].index ?? 0;
|
|
33
|
+
const end = headings[i + 1]?.index ?? tasksText.length;
|
|
34
|
+
const block = tasksText.slice(start, end);
|
|
35
|
+
const id = headings[i][1];
|
|
36
|
+
const title = headings[i][2].trim();
|
|
37
|
+
let requirement = null;
|
|
38
|
+
for (const match of block.matchAll(TASK_FIELD)) {
|
|
39
|
+
if (match[1].trim().toLowerCase() === "requirement") {
|
|
40
|
+
requirement = match[2].trim();
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const complete = /-\s*\[x\]\s*complete\b/i.test(block);
|
|
45
|
+
tasks.push({ id, title, requirement, complete });
|
|
46
|
+
}
|
|
47
|
+
return tasks;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} validationText
|
|
52
|
+
* @param {string} reqId
|
|
53
|
+
* @returns {string | null}
|
|
54
|
+
*/
|
|
55
|
+
function evidenceForReq(validationText, reqId) {
|
|
56
|
+
for (const line of validationText.split(/\r?\n/)) {
|
|
57
|
+
if (!line.includes(reqId)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const hit = line.match(EVIDENCE);
|
|
61
|
+
if (hit?.[0]) {
|
|
62
|
+
return hit[0];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} specText
|
|
70
|
+
* @returns {string | null}
|
|
71
|
+
*/
|
|
72
|
+
function summarizeGoal(specText) {
|
|
73
|
+
const title = specText.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
74
|
+
if (title && !/^spec/i.test(title)) {
|
|
75
|
+
return title;
|
|
76
|
+
}
|
|
77
|
+
const firstReq = specText.match(
|
|
78
|
+
/^###\s+([A-Z][A-Z0-9]{1,9}-\d{2,4})\b[^\n]*\n+([^\n#]+)/m,
|
|
79
|
+
);
|
|
80
|
+
if (firstReq) {
|
|
81
|
+
return `${firstReq[1]} — ${firstReq[2].trim()}`;
|
|
82
|
+
}
|
|
83
|
+
return title ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} [featureArg]
|
|
88
|
+
* @param {{ cwd?: string }} [options]
|
|
89
|
+
*/
|
|
90
|
+
export async function buildFeatureOverview(featureArg, options = {}) {
|
|
91
|
+
const cwd = options.cwd ?? process.cwd();
|
|
92
|
+
const featureId = await resolveFeatureId(featureArg, cwd);
|
|
93
|
+
const status = await featureStatus(featureId, { cwd });
|
|
94
|
+
const dir = featureDir(featureId, cwd);
|
|
95
|
+
|
|
96
|
+
let specText = "";
|
|
97
|
+
let tasksText = "";
|
|
98
|
+
let validationText = "";
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
specText = await readFileSafe(path.join(dir, "spec.md"));
|
|
102
|
+
} catch {
|
|
103
|
+
// optional until Specify
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
tasksText = await readFileSafe(path.join(dir, "tasks.md"));
|
|
107
|
+
} catch {
|
|
108
|
+
// optional until Tasks
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
validationText = await readFileSafe(path.join(dir, "validation.md"));
|
|
112
|
+
} catch {
|
|
113
|
+
// optional until Verify
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const reqIds = uniqueReqIds(specText);
|
|
117
|
+
const tasks = tasksText ? parseTasks(tasksText) : [];
|
|
118
|
+
const reqToTasks = new Map();
|
|
119
|
+
for (const task of tasks) {
|
|
120
|
+
if (!task.requirement) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
for (const reqId of uniqueReqIds(task.requirement)) {
|
|
124
|
+
const list = reqToTasks.get(reqId) ?? [];
|
|
125
|
+
list.push(task.id);
|
|
126
|
+
reqToTasks.set(reqId, list);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const traceability = reqIds.map((reqId) => ({
|
|
131
|
+
reqId,
|
|
132
|
+
tasks: reqToTasks.get(reqId) ?? [],
|
|
133
|
+
evidence: validationText ? evidenceForReq(validationText, reqId) : null,
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
featureId,
|
|
138
|
+
generatedAt: new Date().toISOString(),
|
|
139
|
+
goal: specText ? summarizeGoal(specText) : null,
|
|
140
|
+
status,
|
|
141
|
+
tasks,
|
|
142
|
+
traceability,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {Awaited<ReturnType<typeof buildFeatureOverview>>} overview
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function formatFeatureOverview(overview) {
|
|
151
|
+
const { status } = overview;
|
|
152
|
+
const artifactRows = Object.entries(status.artifacts)
|
|
153
|
+
.map(([name, present]) => `| ${name} | ${present ? "present" : "missing"} |`)
|
|
154
|
+
.join("\n");
|
|
155
|
+
|
|
156
|
+
const taskRows =
|
|
157
|
+
overview.tasks.length > 0
|
|
158
|
+
? overview.tasks
|
|
159
|
+
.map(
|
|
160
|
+
(task) =>
|
|
161
|
+
`| ${task.id} | ${task.title} | ${task.requirement ?? "—"} | ${task.complete ? "done" : "open"} |`,
|
|
162
|
+
)
|
|
163
|
+
.join("\n")
|
|
164
|
+
: "| — | — | — | — |";
|
|
165
|
+
|
|
166
|
+
const traceRows =
|
|
167
|
+
overview.traceability.length > 0
|
|
168
|
+
? overview.traceability
|
|
169
|
+
.map((row) => {
|
|
170
|
+
const tasks =
|
|
171
|
+
row.tasks.length > 0 ? row.tasks.join(", ") : "—";
|
|
172
|
+
return `| ${row.reqId} | ${tasks} | ${row.evidence ?? "—"} |`;
|
|
173
|
+
})
|
|
174
|
+
.join("\n")
|
|
175
|
+
: "| — | — | — |";
|
|
176
|
+
|
|
177
|
+
const taskSummary = status.tasks
|
|
178
|
+
? `${status.tasks.complete}/${status.tasks.total} complete (${status.tasks.open} open)`
|
|
179
|
+
: "—";
|
|
180
|
+
|
|
181
|
+
return `# Feature overview: ${overview.featureId}
|
|
182
|
+
|
|
183
|
+
> Generated ${overview.generatedAt}. Refresh with \`feature-overview ${overview.featureId} --write\`.
|
|
184
|
+
|
|
185
|
+
## Summary
|
|
186
|
+
|
|
187
|
+
| Field | Value |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| Goal | ${overview.goal ?? "—"} |
|
|
190
|
+
| Phase | ${status.phase ?? "—"} |
|
|
191
|
+
| Branch | ${status.branch ?? "—"} |
|
|
192
|
+
| Tasks | ${taskSummary} |
|
|
193
|
+
| Validation | ${status.verdict ?? "—"} |
|
|
194
|
+
| Next | ${status.next} |
|
|
195
|
+
|
|
196
|
+
## Artifacts
|
|
197
|
+
|
|
198
|
+
| Artifact | Status |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
${artifactRows}
|
|
201
|
+
|
|
202
|
+
## Tasks
|
|
203
|
+
|
|
204
|
+
| Task | Title | Requirement | Status |
|
|
205
|
+
| --- | --- | --- | --- |
|
|
206
|
+
${taskRows}
|
|
207
|
+
|
|
208
|
+
## Traceability (REQ → task → evidence)
|
|
209
|
+
|
|
210
|
+
| REQ | Task(s) | Test evidence |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
${traceRows}
|
|
213
|
+
|
|
214
|
+
_Evidence rows populate after \`validation.md\` exists. Structural gaps are caught by \`validate-traceability\`._
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @param {string} [featureArg]
|
|
220
|
+
* @param {{ cwd?: string; write?: boolean }} [options]
|
|
221
|
+
*/
|
|
222
|
+
export async function featureOverview(featureArg, options = {}) {
|
|
223
|
+
const cwd = options.cwd ?? process.cwd();
|
|
224
|
+
const overview = await buildFeatureOverview(featureArg, { cwd });
|
|
225
|
+
const markdown = formatFeatureOverview(overview);
|
|
226
|
+
|
|
227
|
+
if (options.write) {
|
|
228
|
+
const outPath = path.join(
|
|
229
|
+
featureDir(overview.featureId, cwd),
|
|
230
|
+
"overview.md",
|
|
231
|
+
);
|
|
232
|
+
await fs.writeFile(outPath, markdown, "utf8");
|
|
233
|
+
overview.writtenTo = outPath;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return overview;
|
|
237
|
+
}
|
package/lib/gates.js
CHANGED
|
@@ -229,6 +229,18 @@ export async function runGuardrailsScriptCapture(command, args, options = {}) {
|
|
|
229
229
|
const cwd = options.cwd ?? process.cwd();
|
|
230
230
|
const scriptsDir = await resolveScriptsDir(cwd);
|
|
231
231
|
const scriptPath = path.join(cwd, scriptsDir, scriptName);
|
|
232
|
+
|
|
233
|
+
for (const required of [scriptName, "_common.py"]) {
|
|
234
|
+
try {
|
|
235
|
+
await access(path.join(cwd, scriptsDir, required), constants.R_OK);
|
|
236
|
+
} catch {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Guardrails script not found at ${path.join(scriptsDir, required)}. ` +
|
|
239
|
+
`Run \`${NPX("install")}\` in this project first.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
232
244
|
const python = await resolvePython();
|
|
233
245
|
|
|
234
246
|
if (!python) {
|
package/lib/install.js
CHANGED
|
@@ -167,7 +167,7 @@ export async function install(options = {}) {
|
|
|
167
167
|
const pythonAvailable = await hasPython();
|
|
168
168
|
if (!pythonAvailable) {
|
|
169
169
|
log(
|
|
170
|
-
"⚠️ Python 3 not found — Process mode only (flexible workflow, manual checkpoints). " +
|
|
170
|
+
"⚠️ Python 3.10+ not found — Process mode only (flexible workflow, manual checkpoints). " +
|
|
171
171
|
"Install Python 3.10+ for Brakes mode (full kit with automatic gates).",
|
|
172
172
|
);
|
|
173
173
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luizsantiago/spec-guardrails",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Governed spec-driven development for AI coding agents. Your agent writes the spec, gets your approval, builds in small waves, and proves the result — plans and project memory stored as files in your repo.",
|
|
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_platform_detect.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 test/test_validation_verdict.test.js test/test_execution_policy.test.js test/test_workspace_isolation.test.js test/test_adapter_registry.test.js test/test_context_guard.test.js test/test_solution_exploration.test.js test/test_memory_doctor.test.js test/test_cursor_hooks_cleanup.test.js test/test_sandbox_policy.test.js test/test_req_analysis.test.js",
|
|
15
|
+
"test:node": "node --test test/install.test.js test/test_platform_detect.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_feature_overview.test.js test/test_agent_contract.test.js test/test_gates_python.test.js test/test_specs_utils.test.js test/test_validation_verdict.test.js test/test_execution_policy.test.js test/test_workspace_isolation.test.js test/test_adapter_registry.test.js test/test_context_guard.test.js test/test_solution_exploration.test.js test/test_memory_doctor.test.js test/test_cursor_hooks_cleanup.test.js test/test_sandbox_policy.test.js test/test_req_analysis.test.js",
|
|
16
16
|
"test:gates": "node test/run-gate-tests.mjs",
|
|
17
17
|
"prepublishOnly": "npm test"
|
|
18
18
|
},
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import re
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
CONFIG_RELATIVE = Path(".specs") / "config.yaml"
|
|
9
9
|
|
|
10
10
|
DEFAULT_SUPPRESSION_PATTERNS = [
|
|
11
11
|
r"#\s*noqa\b",
|
|
@@ -22,6 +22,11 @@ DEFAULT_SUPPRESSION_PATTERNS = [
|
|
|
22
22
|
DEFAULT_MAX_STAGED_LINES = 500
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def config_path(cwd: Path | str | None = None) -> Path:
|
|
26
|
+
root = Path(cwd) if cwd is not None else Path.cwd()
|
|
27
|
+
return root / CONFIG_RELATIVE
|
|
28
|
+
|
|
29
|
+
|
|
25
30
|
def _parse_scalar(raw: str):
|
|
26
31
|
value = raw.strip().strip("'\"")
|
|
27
32
|
lower = value.lower()
|
|
@@ -52,7 +57,7 @@ def _parse_list_block(lines: list[str], start_index: int, parent_indent: int) ->
|
|
|
52
57
|
return items, index
|
|
53
58
|
|
|
54
59
|
|
|
55
|
-
def load_project_config() -> dict:
|
|
60
|
+
def load_project_config(cwd: Path | str | None = None) -> dict:
|
|
56
61
|
"""Return quality, suppressions, and commit policy blocks with defaults."""
|
|
57
62
|
|
|
58
63
|
config = {
|
|
@@ -61,14 +66,13 @@ def load_project_config() -> dict:
|
|
|
61
66
|
"commit": {"max_staged_lines": DEFAULT_MAX_STAGED_LINES},
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
path = config_path(cwd)
|
|
70
|
+
if not path.is_file():
|
|
65
71
|
return config
|
|
66
72
|
|
|
67
|
-
lines =
|
|
73
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
68
74
|
section = None
|
|
69
75
|
section_indent = 0
|
|
70
|
-
subsection = None
|
|
71
|
-
subsection_indent = 0
|
|
72
76
|
|
|
73
77
|
index = 0
|
|
74
78
|
while index < len(lines):
|
|
@@ -83,27 +87,23 @@ def load_project_config() -> dict:
|
|
|
83
87
|
if stripped == "quality:":
|
|
84
88
|
section = "quality"
|
|
85
89
|
section_indent = indent
|
|
86
|
-
subsection = None
|
|
87
90
|
index += 1
|
|
88
91
|
continue
|
|
89
92
|
|
|
90
93
|
if stripped == "suppressions:":
|
|
91
94
|
section = "suppressions"
|
|
92
95
|
section_indent = indent
|
|
93
|
-
subsection = None
|
|
94
96
|
index += 1
|
|
95
97
|
continue
|
|
96
98
|
|
|
97
99
|
if stripped == "commit:":
|
|
98
100
|
section = "commit"
|
|
99
101
|
section_indent = indent
|
|
100
|
-
subsection = None
|
|
101
102
|
index += 1
|
|
102
103
|
continue
|
|
103
104
|
|
|
104
105
|
if section and indent <= section_indent and not stripped.endswith(":"):
|
|
105
106
|
section = None
|
|
106
|
-
subsection = None
|
|
107
107
|
|
|
108
108
|
if section == "quality" and stripped == "checks:":
|
|
109
109
|
items, index = _parse_list_block(lines, index + 1, indent)
|
package/scripts/check_commit.py
CHANGED
|
@@ -29,7 +29,7 @@ import sys
|
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
|
|
31
31
|
from _common import EXIT_OK, EXIT_USAGE, Report
|
|
32
|
-
from _project_config import load_project_config
|
|
32
|
+
from _project_config import DEFAULT_MAX_STAGED_LINES, load_project_config
|
|
33
33
|
|
|
34
34
|
GATE = "check-commit"
|
|
35
35
|
|
|
@@ -84,8 +84,8 @@ def count_staged_lines(numstat_text: str) -> int:
|
|
|
84
84
|
|
|
85
85
|
def build_staged_report(cwd: Path) -> Report:
|
|
86
86
|
report = Report(gate=GATE, target="staged changes")
|
|
87
|
-
config = load_project_config()
|
|
88
|
-
max_lines = int(config.get("commit", {}).get("max_staged_lines") or
|
|
87
|
+
config = load_project_config(cwd)
|
|
88
|
+
max_lines = int(config.get("commit", {}).get("max_staged_lines") or DEFAULT_MAX_STAGED_LINES)
|
|
89
89
|
|
|
90
90
|
if not (cwd / ".git").exists():
|
|
91
91
|
report.error("not a git repository")
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Run before commit when agents might silence linters or skip hooks:
|
|
5
5
|
|
|
6
6
|
python3 check_suppressions.py
|
|
7
|
-
python3 check_suppressions.py --
|
|
7
|
+
python3 check_suppressions.py --cwd /path/to/repo
|
|
8
8
|
|
|
9
9
|
Scans `git diff --cached` for patterns such as `# noqa`, `eslint-disable`,
|
|
10
10
|
`@ts-ignore`, skipped tests, or `--no-verify`. Patterns are configurable under
|
|
@@ -86,11 +86,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
86
86
|
default=".",
|
|
87
87
|
help="repository root (default: current directory)",
|
|
88
88
|
)
|
|
89
|
-
parser.add_argument(
|
|
90
|
-
"--strict",
|
|
91
|
-
action="store_true",
|
|
92
|
-
help="treat warnings as blocking failures",
|
|
93
|
-
)
|
|
94
89
|
args = parser.parse_args(argv)
|
|
95
90
|
|
|
96
91
|
cwd = Path(args.cwd).resolve()
|
|
@@ -99,7 +94,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
99
94
|
print(" error not a git repository")
|
|
100
95
|
return EXIT_USAGE
|
|
101
96
|
|
|
102
|
-
config = load_project_config()
|
|
97
|
+
config = load_project_config(cwd)
|
|
103
98
|
patterns = config.get("suppressions", {}).get("patterns") or DEFAULT_SUPPRESSION_PATTERNS
|
|
104
99
|
|
|
105
100
|
try:
|
|
@@ -110,7 +105,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
110
105
|
return EXIT_USAGE
|
|
111
106
|
|
|
112
107
|
report = build_report(diff_text, patterns)
|
|
113
|
-
return report.emit(
|
|
108
|
+
return report.emit()
|
|
114
109
|
|
|
115
110
|
|
|
116
111
|
if __name__ == "__main__":
|
|
@@ -14,7 +14,7 @@ Configure under:
|
|
|
14
14
|
- npm test
|
|
15
15
|
- npm run lint
|
|
16
16
|
|
|
17
|
-
Exit codes: 0 all passed, 1 one or more failed
|
|
17
|
+
Exit codes: 0 all passed or skipped (no checks configured), 1 one or more failed.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
@@ -61,7 +61,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
61
61
|
args = parser.parse_args(argv)
|
|
62
62
|
|
|
63
63
|
cwd = Path(args.cwd).resolve()
|
|
64
|
-
checks = load_project_config().get("quality", {}).get("checks") or []
|
|
64
|
+
checks = load_project_config(cwd).get("quality", {}).get("checks") or []
|
|
65
65
|
|
|
66
66
|
if not checks:
|
|
67
67
|
message = {
|
|
@@ -59,7 +59,7 @@ Structural gates run **before** owner review, so they cannot drift when the mode
|
|
|
59
59
|
| Retrieve related context | `npx @luizsantiago/spec-guardrails memory-retrieve "<query>"` |
|
|
60
60
|
| Rebuild / embed memory index | `npx @luizsantiago/spec-guardrails memory-index rebuild` · `memory-index embed` |
|
|
61
61
|
| On gate retry (Execute playbook) | `npx @luizsantiago/spec-guardrails execution-policy record-retry Tn` |
|
|
62
|
-
| On each commit | `python3 .specs/guardrails/scripts/check_commit.py --message "<message>"` |
|
|
62
|
+
| On each commit | `python3 .specs/guardrails/scripts/check_commit.py --message "<message>"` · `check_commit.py --staged` |
|
|
63
63
|
| Before each commit (staged diff) | `python3 .specs/guardrails/scripts/check_suppressions.py` |
|
|
64
64
|
| During Verify, when `quality.checks` is configured | `python3 .specs/guardrails/scripts/run_quality_checks.py` |
|
|
65
65
|
| Before declaring a feature done | `python3 .specs/guardrails/scripts/validate_state.py [feature]` |
|
|
@@ -67,6 +67,7 @@ Structural gates run **before** owner review, so they cannot drift when the mode
|
|
|
67
67
|
| Quick mode evidence | `python3 .specs/guardrails/scripts/validate_quick.py [feature]` |
|
|
68
68
|
| After Verify PASS | `npx @luizsantiago/spec-guardrails archive-feature [feature]` (Tier 0) |
|
|
69
69
|
| Before a phase procedure (optional) | `npx @luizsantiago/spec-guardrails phase-context <phase>` |
|
|
70
|
+
| Feature dashboard (human-readable) | `npx @luizsantiago/spec-guardrails feature-overview [feature] [--write]` |
|
|
70
71
|
| After a FAIL verdict | `python3 .specs/guardrails/scripts/lessons.py add --source .specs/features/[feature]/validation.md` |
|
|
71
72
|
|
|
72
73
|
Gates accept a feature name, a feature directory, or a path to the artifact. With no argument they auto-detect when the project has exactly one feature; with several they list candidates and exit 2. A spec is rejected unless every criterion uses `SHALL` or `MUST` and `## Assumptions` is present.
|
|
@@ -92,11 +93,11 @@ EXPLORE (optional) → ELICIT (optional) → SPECIFY → DISCUSS (conditional)
|
|
|
92
93
|
| **Tasks** | No | `references/tasks.md` | `task-graph-engineering.md` | `validate_tasks.py` |
|
|
93
94
|
| **Analyze** | Before task approval | `references/analyze.md` | — | `analyze_artifacts.py` |
|
|
94
95
|
| **Execute** | Yes | `references/implement.md` | `engineering-standards.md` | `check_commit.py`, `check_suppressions.py` |
|
|
95
|
-
| **Verify** | Yes | `references/validate.md` | `security-review.md` | `validate_state.py`, `run_quality_checks.py` |
|
|
96
|
+
| **Verify** | Yes | `references/validate.md` | `security-review.md` | `validate_traceability.py`, `validate_state.py`, `run_quality_checks.py` |
|
|
96
97
|
| **Archive** | After Verify PASS | `references/archive.md` | `git-handoff.md` | `archive-feature` |
|
|
97
98
|
| **Converge** | On drift | `references/converge.md` | — | `analyze_artifacts.py` |
|
|
98
99
|
| **Handoff** | Yes | `references/memory.md` | `git-handoff.md` | — |
|
|
99
|
-
| **Quick** | Alternative | `references/quick-mode.md` | — | `check_commit.py`, `validate_quick.py` |
|
|
100
|
+
| **Quick** | Alternative | `references/quick-mode.md` | — | `check_commit.py`, `check_suppressions.py`, `validate_quick.py` |
|
|
100
101
|
| **Context** | Always | `references/context-limits.md` | — | — |
|
|
101
102
|
| **Sub-agents** | When batched | `references/sub-agents.md` | `task-graph-engineering.md` | — |
|
|
102
103
|
| **Solution exploration** | Explicit fork | `references/solution-exploration.md` | — | `solution-explore validate` |
|
|
@@ -121,7 +122,7 @@ Complexity determines depth. Do not run every phase on every change.
|
|
|
121
122
|
|
|
122
123
|
| Tier | Scope | Path |
|
|
123
124
|
| --- | --- | --- |
|
|
124
|
-
| **Quick** | ≤3 files, no design decisions, no new dependencies | `references/quick-mode.md` — describe, implement, verify, commit; gates: `check_commit.py`
|
|
125
|
+
| **Quick** | ≤3 files, no design decisions, no new dependencies | `references/quick-mode.md` — describe, implement, verify, commit; gates: `check_commit.py`, `check_suppressions.py`, `validate-quick` |
|
|
125
126
|
| **Simple** | 2–5 files, localized change | Specify → Execute → Verify |
|
|
126
127
|
| **Medium** | New feature, <10 tasks | Specify → Tasks → Execute → Verify |
|
|
127
128
|
| **Complex** | New architecture, API surface, infra | Specify → Discuss → Design → Tasks → Execute → Verify |
|
|
@@ -131,7 +132,7 @@ Complexity determines depth. Do not run every phase on every change.
|
|
|
131
132
|
|
|
132
133
|
**Rules**
|
|
133
134
|
|
|
134
|
-
- **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.
|
|
135
|
+
- **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`, `check_suppressions.py` on each commit, and `validate-quick` as the close/evidence gate.
|
|
135
136
|
- **Design is skipped** when there are no architectural decisions and no new patterns.
|
|
136
137
|
- **Tasks is skipped** when there are ≤3 obvious steps.
|
|
137
138
|
- **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.
|
|
@@ -28,10 +28,12 @@ Updated `tasks.md` with new tasks for uncovered work (append only — do not rew
|
|
|
28
28
|
|
|
29
29
|
1. **Re-read the spec** — list every REQ and whether tests exist for its outcome.
|
|
30
30
|
2. **Audit the diff** — files changed outside task `Files` fields are scope drift or missing tasks.
|
|
31
|
-
3. **
|
|
32
|
-
4. **
|
|
33
|
-
5. **
|
|
34
|
-
6. **
|
|
31
|
+
3. **Living-spec drift (lightweight)** — when `domains/<slug>/spec.md` exists, spot-check that files still match declared globs or modules; append a task if production code moved without updating domain memory.
|
|
32
|
+
4. **Run analyze** — `analyze_artifacts.py [feature]`.
|
|
33
|
+
5. **Append tasks** for gaps using the standard task template in `tasks.md`.
|
|
34
|
+
6. **Refresh dashboard** — `npx @luizsantiago/spec-guardrails feature-overview [feature] --write`.
|
|
35
|
+
7. **Update STATE** Next Step to the first open task.
|
|
36
|
+
8. **Commit** `.specs/` changes (Tier 0) — no push.
|
|
35
37
|
|
|
36
38
|
## Rules
|
|
37
39
|
|
|
@@ -64,8 +64,9 @@ loop-plan → dispatch (parallel sub-agents | inline) → merge → loop-plan
|
|
|
64
64
|
2. **Dispatch**
|
|
65
65
|
- **Parallel group (2+ tasks):** one sub-agent per task with the worker brief in `sub-agents.md`. Cap at 3 workers + 1 verifier (see `task-graph-engineering.md`).
|
|
66
66
|
- **Single task:** run the per-task cycle below inline (or as sole worker).
|
|
67
|
-
3. **
|
|
68
|
-
4. **
|
|
67
|
+
3. **Two-stage batch review** — After workers return and before merge, the orchestrator reviews each batch twice (see `sub-agents.md`): spec compliance, then code quality. Do not merge until both pass.
|
|
68
|
+
4. **Merge** — Confirm every task is `[x]`, commits exist, and the project harness passes once on the integrated tree.
|
|
69
|
+
5. **Repeat** — Run `loop-plan` again until all tasks are complete, then close Execute.
|
|
69
70
|
|
|
70
71
|
Do not start the next wave until the current wave is fully committed. Parallelism is **inside** a wave only when `Files` do not overlap.
|
|
71
72
|
|
|
@@ -194,8 +195,9 @@ docs(spec): record validation report for auth
|
|
|
194
195
|
When the last task is complete:
|
|
195
196
|
|
|
196
197
|
1. Run the full project harness once more (tests, linter, build).
|
|
197
|
-
2.
|
|
198
|
-
3.
|
|
198
|
+
2. Refresh the feature dashboard: `npx @luizsantiago/spec-guardrails feature-overview [feature] --write` (writes `overview.md`).
|
|
199
|
+
3. Trigger `/verify` with a fresh context — mandatory, never prompted. See `validate.md`.
|
|
200
|
+
4. Do not declare the feature done until `validate_state.py` passes.
|
|
199
201
|
|
|
200
202
|
## Next
|
|
201
203
|
|
|
@@ -69,6 +69,23 @@ Every worker returns this shape — no narrative dump:
|
|
|
69
69
|
- Blockers: none | [gate output excerpt]
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## Two-stage batch review (orchestrator)
|
|
73
|
+
|
|
74
|
+
After workers return and **before** merge, the orchestrator runs both stages. Workers do not self-certify merge readiness.
|
|
75
|
+
|
|
76
|
+
| Stage | Question | Evidence |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| **1 — Spec compliance** | Does each commit satisfy its task `Done when` and the REQ IDs on that task? | Re-read task blocks + diff; compare to spec criteria |
|
|
79
|
+
| **2 — Code quality** | Is the integrated harness green, free of new suppressions, and scoped to task `Files`? | Project harness + `check_suppressions.py` on staged diff |
|
|
80
|
+
|
|
81
|
+
| Outcome | Action |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| Stage 1 FAIL | Send worker back, or rewrite the task — do not merge |
|
|
84
|
+
| Stage 2 FAIL | Fix or revert before merge — do not weaken tests |
|
|
85
|
+
| Both PASS | Merge owner integrates worktrees, runs harness once, proceeds |
|
|
86
|
+
|
|
87
|
+
Pattern adapted from [Superpowers subagent-driven-development](https://github.com/obra/superpowers) — compliance before polish.
|
|
88
|
+
|
|
72
89
|
## Failure handling
|
|
73
90
|
|
|
74
91
|
| Event | Action |
|
|
@@ -26,6 +26,22 @@ For changes with **no** auth, API, user input, payments, or infrastructure impac
|
|
|
26
26
|
|
|
27
27
|
Full OWASP checklist remains mandatory for anything touching auth, data, APIs, or infra.
|
|
28
28
|
|
|
29
|
+
## Third-party agent skills (supply chain)
|
|
30
|
+
|
|
31
|
+
Spec Guardrails ships skills you install with `npx @luizsantiago/spec-guardrails install`. When adding **other** skill packs from GitHub, marketplaces (`skills.sh`, plugin stores), or zip files:
|
|
32
|
+
|
|
33
|
+
1. **Read before trust** — inspect `SKILL.md` and any bundled scripts; skills run with agent privileges.
|
|
34
|
+
2. **Scan when risk is non-trivial** — use [NVIDIA SkillSpector](https://github.com/NVIDIA/SkillSpector) for static (and optional LLM) analysis before install:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
skillspector scan https://github.com/user/some-skill --no-llm
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
3. **Prefer signed catalogs** — NVIDIA Verified Skills and official plugin marketplaces reduce supply-chain risk; still review high-privilege skills.
|
|
41
|
+
4. **Never bypass Brakes** — third-party skills do not replace Spec Guardrails gates (`check-suppressions`, `validate-state`, …).
|
|
42
|
+
|
|
43
|
+
For publication-quality skill evaluation (overlap detection, live agent eval), see [SkillEvaluator](https://github.com/NVIDIA/SkillEvaluator) — complementary to this package, not a replacement.
|
|
44
|
+
|
|
29
45
|
## Pre-Review Setup
|
|
30
46
|
|
|
31
47
|
- Verifier must have **clean context** (not the code author).
|
|
@@ -72,6 +72,7 @@ The shape serious systems converge to for splittable work:
|
|
|
72
72
|
| --- | --- | --- |
|
|
73
73
|
| Plan | Single planner | Produces `tasks.md` + `task-graph.md` |
|
|
74
74
|
| Workers | 1–3 parallel agents | Disjoint file ownership only |
|
|
75
|
+
| Batch review | Orchestrator (two stages) | Spec compliance, then code quality — before merge |
|
|
75
76
|
| Verify | Fresh verifier context | Never the code author |
|
|
76
77
|
| Merge | One merge owner | Resolves conflicts, runs harness |
|
|
77
78
|
| Result | Commits + evidence | Handoff to `/archive` when done |
|
|
@@ -83,12 +83,17 @@ sandbox:
|
|
|
83
83
|
- "\\b(DROP\\s+DATABASE|DROP\\s+SCHEMA|TRUNCATE\\s+TABLE)\\b"
|
|
84
84
|
|
|
85
85
|
# Verification suppressions (optional — block agents from silencing linters/tests)
|
|
86
|
+
# Omit this block to keep runtime defaults from _project_config.py (9 patterns).
|
|
86
87
|
suppressions:
|
|
87
88
|
patterns:
|
|
88
89
|
- "#\\s*noqa\\b"
|
|
89
|
-
- "
|
|
90
|
+
- "nosemgrep\\b"
|
|
91
|
+
- "eslint-disable(?:-next-line|-line)?"
|
|
90
92
|
- "@ts-ignore\\b"
|
|
93
|
+
- "@ts-expect-error\\b"
|
|
91
94
|
- "\\bxit\\s*\\("
|
|
95
|
+
- "\\bxdescribe\\s*\\("
|
|
96
|
+
- "\\bpytest\\.mark\\.skip\\b"
|
|
92
97
|
- "--no-verify\\b"
|
|
93
98
|
|
|
94
99
|
# Project quality commands (optional — run during /verify)
|