@luizsantiago/spec-guardrails 4.5.2 → 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 CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
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.
12
12
 
13
- npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.5.x**
13
+ npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.6.x**
14
14
 
15
15
  ---
16
16
 
@@ -60,7 +60,7 @@ In short: Python turns "trust the agent" into "the agent has to prove it."
60
60
 
61
61
  Which checks exist and what each one requires: [Gates](docs/guide/gates.md) · [Guarantees matrix](docs/guide/Guarantees-matrix.md)
62
62
 
63
- 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)
64
64
 
65
65
  ---
66
66
 
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";
@@ -105,6 +109,9 @@ Commands:
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
@@ -1079,6 +1086,33 @@ if (command === "--version" || command === "-v" || command === "version") {
1079
1086
  console.error(`❌ ${err.message}`);
1080
1087
  process.exit(1);
1081
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
+ }
1082
1116
  } else if (AUX_COMMANDS.includes(command)) {
1083
1117
  try {
1084
1118
  const code = await runGuardrailsScript(command, args);
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luizsantiago/spec-guardrails",
3
- "version": "4.5.2",
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
  },
@@ -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.
@@ -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. **Run analyze** — `analyze_artifacts.py [feature]`.
32
- 4. **Append tasks** for gaps using the standard task template in `tasks.md`.
33
- 5. **Update STATE** Next Step to the first open task.
34
- 6. **Commit** `.specs/` changes (Tier 0) no push.
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. **Merge** — After a parallel round, confirm every task is `[x]`, commits exist, and the project harness passes once on the integrated tree.
68
- 4. **Repeat** — Run `loop-plan` again until all tasks are complete, then close Execute.
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. Trigger `/verify` with a fresh context mandatory, never prompted. See `validate.md`.
198
- 3. Do not declare the feature done until `validate_state.py` passes.
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 |