@luizsantiago/spec-guardrails 3.9.0 → 4.0.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 +105 -86
- package/index.js +70 -0
- package/lib/constants.js +1 -0
- package/lib/req-analysis.js +274 -0
- package/package.json +2 -2
- package/scripts/memory_index.py +64 -0
- package/skills/agent-architecture.md +9 -2
- package/skills/references/elicitation.md +197 -0
- package/skills/references/explore.md +1 -0
- package/skills/references/specify.md +1 -0
- package/templates/config.yaml.example +9 -0
package/README.md
CHANGED
|
@@ -3,156 +3,175 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@luizsantiago/spec-guardrails)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
**
|
|
6
|
+
**Governed spec-driven development for AI coding agents.**
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Spec Guardrails is a process kit that installs phase guides, persistent project memory, and optional structural checks into your repository. Teams keep ownership of requirements and approval gates; agents follow a repeatable path from written intent to verified delivery.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
npm: [`@luizsantiago/spec-guardrails`](https://www.npmjs.com/package/@luizsantiago/spec-guardrails) **4.0.x**
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
---
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## What it is
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
2. **Break work into small jobs** (`tasks.md`)
|
|
18
|
-
3. **Implement in waves** with real checks
|
|
19
|
-
4. **Verify with fresh eyes** — proof, not “trust me”
|
|
16
|
+
Spec Guardrails is **not** an application framework, a vector database, or a replacement for your stack. It is an **operating model** for AI-assisted engineering:
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
| Layer | Role |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| **Phase guides** | Instructions the agent loads one step at a time |
|
|
21
|
+
| **Project memory** | `.specs/` — specs, tasks, validation, and state that survive chat sessions |
|
|
22
|
+
| **Structural checks** | Optional Python gates that enforce document shape and evidence hooks |
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
The default loop is **Specify → Tasks → Execute → Verify → Archive**. You approve specs and task plans; the agent implements in waves and produces proof before work is considered done.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Full narrative: [Overview](docs/guide/Overview.md) · [How it works](docs/guide/How-it-works.md)
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
29
|
-
##
|
|
30
|
+
## Why teams use it
|
|
31
|
+
|
|
32
|
+
| Benefit | Outcome |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| **Traceability** | Requirements, jobs, and verification live in version-controlled artifacts |
|
|
35
|
+
| **Controlled autonomy** | Agents propose and execute; humans approve scope, design forks, and git tiers |
|
|
36
|
+
| **Progressive depth** | Quick fixes skip ceremony; complex work gets discuss, design, and task graphs |
|
|
37
|
+
| **Platform-agnostic** | Cursor, Claude Code, GitHub Copilot, Codex, and other agents via root `AGENTS.md` |
|
|
38
|
+
| **Token efficiency** | One phase guide per turn instead of dumping the entire playbook |
|
|
39
|
+
| **Optional enforcement** | Process mode (Node only) or Brakes mode (Node + Python gates) |
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
- Not a vector database or “AI memory” that reads your whole codebase
|
|
33
|
-
- Not automatic push/merge/deploy — you stay in control of git tiers
|
|
34
|
-
- Not a replacement for code review or product judgment
|
|
41
|
+
Process vs Brakes: [FAQ](docs/guide/FAQ.md#process-vs-brakes) · Guarantees: [Guarantees matrix](docs/guide/Guarantees-matrix.md)
|
|
35
42
|
|
|
36
43
|
---
|
|
37
44
|
|
|
38
|
-
## Install
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
In your project root:
|
|
39
48
|
|
|
40
49
|
```bash
|
|
41
50
|
npx @luizsantiago/spec-guardrails install
|
|
42
51
|
```
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
Re-run after upgrading the package; existing `.specs/` notes are preserved. Check readiness with `doctor`.
|
|
54
|
+
|
|
55
|
+
| Requirement | Role |
|
|
45
56
|
| --- | --- |
|
|
46
57
|
| **Node.js 18+** | Required — CLI and install |
|
|
47
|
-
| **Python 3.10+** | Optional — enables
|
|
48
|
-
|
|
49
|
-
Re-run `install` after upgrading the package. Your `.specs/` notes and `STATE.md` are kept.
|
|
58
|
+
| **Python 3.10+** | Optional — enables Brakes mode (automatic gates) |
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Works with **Cursor, Claude Code, GitHub Copilot, OpenAI Codex**, and other agents via root `AGENTS.md`. See [Platform parity](docs/guide/Platform-parity.md).
|
|
60
|
+
Platform setup: [Platform parity](docs/guide/Platform-parity.md) · Release notes: [CHANGELOG](docs/CHANGELOG.md)
|
|
54
61
|
|
|
55
62
|
---
|
|
56
63
|
|
|
57
|
-
## How you
|
|
64
|
+
## How you work day to day
|
|
65
|
+
|
|
66
|
+
Interaction happens in **agent chat**, not the terminal. The agent runs CLI helpers and gates when needed.
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
### Core commands
|
|
60
69
|
|
|
61
|
-
| Command |
|
|
70
|
+
| Command | Use when |
|
|
62
71
|
| --- | --- |
|
|
63
|
-
| `/specify` |
|
|
64
|
-
| `/
|
|
65
|
-
| `/
|
|
66
|
-
| `/
|
|
67
|
-
| `/
|
|
72
|
+
| `/specify` | Starting any non-trivial feature — requirements in writing first |
|
|
73
|
+
| `/elicit` | Kickoff or request is vague — structured Q&A before Specify *(optional)* |
|
|
74
|
+
| `/tasks` | Spec approved — break work into a job list |
|
|
75
|
+
| `/loop` | Implementation — one wave at a time |
|
|
76
|
+
| `/verify` | All jobs done — independent proof *(prefer a fresh chat)* |
|
|
77
|
+
| `/archive` | Feature validated — fold into project memory |
|
|
78
|
+
| `/quick` | Tiny fix only (≤3 files, no design fork) |
|
|
68
79
|
|
|
69
80
|
**Typical path:** `/specify` → approve → `/tasks` → approve → `/loop` → `/verify` → `/archive`
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
When input is still exploratory, `/explore` or `/elicit` may come first. The agent suggests depth; it does not block Specify without approval.
|
|
72
83
|
|
|
73
|
-
|
|
84
|
+
Command reference: [Agent commands](docs/guide/agent-commands.md) · Entry paths: [Overview → Three ways to start](docs/guide/Overview.md#three-ways-to-start-pick-one)
|
|
74
85
|
|
|
75
|
-
|
|
86
|
+
### Optional capabilities
|
|
76
87
|
|
|
77
|
-
|
|
88
|
+
Enable when the work warrants them — most teams start with the core loop only.
|
|
78
89
|
|
|
79
|
-
|
|
|
80
|
-
| --- | --- |
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
| Capability | Purpose |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| Memory search | Retrieve past specs, validations, and kickoff briefs |
|
|
93
|
+
| Context guards | Scope check before edit or “done” (Cursor: hooks) |
|
|
94
|
+
| Episodic memory | Session notes → lessons for future runs |
|
|
95
|
+
| Code index | Lightweight brownfield file and symbol map |
|
|
96
|
+
| Solution exploration | Compare implementation options before committing |
|
|
97
|
+
| Sandbox / execution policy | Warn or block destructive shell commands and path drift |
|
|
98
|
+
| Semantic retrieval | Search by meaning — off by default |
|
|
85
99
|
|
|
86
|
-
|
|
100
|
+
Guides: [Memory](docs/guide/Memory.md) · [Brownfield context](docs/guide/brownfield-context.md) · [Safety & exploration](docs/guide/Overview.md)
|
|
87
101
|
|
|
88
102
|
---
|
|
89
103
|
|
|
90
|
-
## What lands in your
|
|
104
|
+
## What lands in your repository
|
|
91
105
|
|
|
92
106
|
| Path | Role |
|
|
93
107
|
| --- | --- |
|
|
94
|
-
| `.cursor/skills/` (
|
|
95
|
-
| `.specs/STATE.md` |
|
|
96
|
-
| `.specs/features/NNN-slug/` | Spec, tasks, validation
|
|
108
|
+
| `.cursor/skills/` (+ Claude, Copilot, Codex trees) | Phase instructions for the agent |
|
|
109
|
+
| `.specs/STATE.md` | Active feature and next step |
|
|
110
|
+
| `.specs/features/NNN-slug/` | Spec, tasks, and validation per feature |
|
|
97
111
|
| `.specs/guardrails/scripts/` | Python gates (Brakes mode) |
|
|
98
|
-
| `.specs/config.yaml` | Optional project rules and policy |
|
|
112
|
+
| `.specs/config.yaml` | Optional project rules and execution policy |
|
|
113
|
+
|
|
114
|
+
Architecture: [Skills and hub](docs/guide/skills-and-hub.md) · [Architecture](docs/guide/Architecture.md)
|
|
99
115
|
|
|
100
116
|
---
|
|
101
117
|
|
|
102
|
-
##
|
|
118
|
+
## Documentation
|
|
103
119
|
|
|
104
|
-
|
|
120
|
+
Start with the guide that matches your question; each page links deeper where needed.
|
|
105
121
|
|
|
106
|
-
|
|
|
122
|
+
| Topic | Start here | Go deeper |
|
|
107
123
|
| --- | --- | --- |
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
| **Solution exploration** | Compare two+ implementations before committing | [Overview → Exploration](docs/guide/Overview.md#optional-exploration-mode) |
|
|
115
|
-
| **Semantic retrieval** | Search by meaning (needs OpenAI or Ollama) | [Memory → Semantic](docs/guide/Memory.md#semantic-search-optional) |
|
|
124
|
+
| Orientation | [Overview](docs/guide/Overview.md) | [Concepts](docs/guide/concepts.md) |
|
|
125
|
+
| First session | [Quick start](docs/guide/Quick-start.md) | [Agent commands](docs/guide/agent-commands.md) |
|
|
126
|
+
| Process model | [How it works](docs/guide/How-it-works.md) | [Loop patterns](docs/guide/loop-patterns.md) |
|
|
127
|
+
| Enforcement | [Gates](docs/guide/gates.md) | [Gates and guarantees](docs/guide/Gates-and-guarantees.md) |
|
|
128
|
+
| Long-running projects | [Memory](docs/guide/Memory.md) | [Brownfield context](docs/guide/brownfield-context.md) |
|
|
129
|
+
| Questions | [FAQ](docs/guide/FAQ.md) | [Stability policy](docs/guide/Stability-policy.md) |
|
|
116
130
|
|
|
117
|
-
|
|
131
|
+
Full index: [docs/guide/README.md](docs/guide/README.md)
|
|
118
132
|
|
|
119
133
|
---
|
|
120
134
|
|
|
121
|
-
##
|
|
135
|
+
## Contributing
|
|
122
136
|
|
|
123
|
-
|
|
124
|
-
| --- | --- |
|
|
125
|
-
| **[Overview](docs/guide/Overview.md)** | Complete simple picture — workflow, layers, team use |
|
|
126
|
-
| [Quick start](docs/guide/Quick-start.md) | First ten minutes |
|
|
127
|
-
| [How it works](docs/guide/How-it-works.md) | Story from idea to archive |
|
|
128
|
-
| [Memory](docs/guide/Memory.md) | `.specs/` memory and search — when and how much |
|
|
129
|
-
| [FAQ](docs/guide/FAQ.md) | Common questions |
|
|
137
|
+
We welcome focused improvements — skills, gates, CLI, docs, and tests. See [CONTRIBUTING.md](CONTRIBUTING.md) for layout, gate stability rules, and local checks.
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
| --- | --- |
|
|
133
|
-
| [Agent commands](docs/guide/agent-commands.md) | Every `/specify`, `/loop`, CLI helper |
|
|
134
|
-
| [Guarantees matrix](docs/guide/Guarantees-matrix.md) | Product promises → mechanisms |
|
|
135
|
-
| [Gates](docs/guide/gates.md) | What each gate checks |
|
|
136
|
-
| [Architecture](docs/guide/Architecture.md) | Core vs platform adapters |
|
|
137
|
-
| [Concepts](docs/guide/concepts.md) | Spec-driven, loop, graph, tiers |
|
|
138
|
-
| [Changelog](docs/CHANGELOG.md) | Version history |
|
|
139
|
+
### Use Spec Guardrails to build your contribution
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
The recommended workflow is to **dogfood the product**: install Spec Guardrails, describe your change through the agent phases, implement against approved artifacts, and verify before opening a PR.
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
**In your own project or fork** — use the latest stable release from npm:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npx @luizsantiago/spec-guardrails@latest install
|
|
147
|
+
npx @luizsantiago/spec-guardrails doctor
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Then in chat: `/specify` (or `/elicit` if scope is unclear) → `/tasks` → `/loop` → `/verify`. Your `.specs/` folder holds the spec and proof that guided the change.
|
|
143
151
|
|
|
144
|
-
|
|
152
|
+
**In this source repository** — work against the branch you are developing, not the published tarball:
|
|
145
153
|
|
|
146
154
|
```bash
|
|
147
|
-
|
|
155
|
+
git clone https://github.com/luizssantiago92/spec-guardrails.git
|
|
156
|
+
cd spec-guardrails
|
|
157
|
+
npm install
|
|
158
|
+
npm run guardrails -- install
|
|
159
|
+
npm run guardrails -- doctor
|
|
148
160
|
```
|
|
149
161
|
|
|
150
|
-
|
|
162
|
+
Edit source under `skills/`, `lib/`, `scripts/`, and `rules/`; re-run `npm run guardrails -- install` after skill or gate changes. Run `npm test` before every PR.
|
|
163
|
+
|
|
164
|
+
| Path | Role |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `skills/` | Hub and sister skills shipped to consumers |
|
|
167
|
+
| `skills/references/` | Phase procedures (`specify.md`, `elicit.md`, …) |
|
|
168
|
+
| `scripts/` | Deterministic Python gates |
|
|
169
|
+
| `test/` | Node install tests and Python gate suites |
|
|
170
|
+
|
|
171
|
+
Gate changes follow the adversarial test policy in [CONTRIBUTING.md](CONTRIBUTING.md). Credits: [docs/guide/credits.md](docs/guide/credits.md)
|
|
151
172
|
|
|
152
173
|
---
|
|
153
174
|
|
|
154
|
-
##
|
|
175
|
+
## License
|
|
155
176
|
|
|
156
|
-
|
|
157
|
-
- [Credits](docs/guide/credits.md)
|
|
158
|
-
- MIT — see [LICENSE](LICENSE)
|
|
177
|
+
MIT — see [LICENSE](LICENSE).
|
package/index.js
CHANGED
|
@@ -35,6 +35,11 @@ import {
|
|
|
35
35
|
listWorkspaces,
|
|
36
36
|
prepareWorkspaces,
|
|
37
37
|
} from "./lib/workspace-isolation.js";
|
|
38
|
+
import {
|
|
39
|
+
formatDiscoverReport,
|
|
40
|
+
formatPromoteMessage,
|
|
41
|
+
reqAnalysisInit,
|
|
42
|
+
} from "./lib/req-analysis.js";
|
|
38
43
|
import {
|
|
39
44
|
formatExplorationInit,
|
|
40
45
|
formatExplorationStatus,
|
|
@@ -76,6 +81,12 @@ Commands:
|
|
|
76
81
|
feature-init "<description>" Allocate NNN-slug feature, STATE, local branch (Tier 0)
|
|
77
82
|
[--no-branch] Skip git checkout -b
|
|
78
83
|
[--no-spec] Skip spec.md stub
|
|
84
|
+
req-analysis init "<description>" Scaffold requirements brief (/elicit phase)
|
|
85
|
+
[--scope project|feature] project = macro brief; feature = one delivery (default: feature)
|
|
86
|
+
[--force] Replace existing brief scaffold
|
|
87
|
+
req-analysis discover List local kickoff sources (prd.md, kickoff.md, …)
|
|
88
|
+
req-analysis promote Print next steps after brief approval
|
|
89
|
+
[--scope project|feature] Match the brief scope
|
|
79
90
|
archive-feature [feature] Fold verified feature into ROADMAP + domain spec; reset STATE
|
|
80
91
|
[--domain <slug>] Domain folder under .specs/domains/ (default: feature slug)
|
|
81
92
|
[--skip-verify] Skip validate-state (tests / recovery only)
|
|
@@ -890,6 +901,65 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
890
901
|
console.error(`❌ ${err.message}`);
|
|
891
902
|
process.exit(1);
|
|
892
903
|
}
|
|
904
|
+
} else if (command === "req-analysis") {
|
|
905
|
+
try {
|
|
906
|
+
const sub = args[0];
|
|
907
|
+
const rest = args.slice(1);
|
|
908
|
+
|
|
909
|
+
if (sub === "init") {
|
|
910
|
+
let scope = "feature";
|
|
911
|
+
let force = false;
|
|
912
|
+
const positional = [];
|
|
913
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
914
|
+
const arg = rest[i];
|
|
915
|
+
if (arg === "--force") {
|
|
916
|
+
force = true;
|
|
917
|
+
} else if (arg === "--scope" && rest[i + 1]) {
|
|
918
|
+
scope = rest[i + 1];
|
|
919
|
+
i += 1;
|
|
920
|
+
} else if (arg.startsWith("--scope=")) {
|
|
921
|
+
scope = arg.slice("--scope=".length);
|
|
922
|
+
} else if (!arg.startsWith("--")) {
|
|
923
|
+
positional.push(arg);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const description = positional.join(" ").trim();
|
|
927
|
+
if (scope === "feature" && !description) {
|
|
928
|
+
throw new Error(
|
|
929
|
+
'Usage: req-analysis init "<description>" [--scope project|feature] [--force]',
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
const result = await reqAnalysisInit(description, { scope, force });
|
|
933
|
+
console.log(`✅ Elicitation scaffold (${result.scope})`);
|
|
934
|
+
for (const briefPath of result.paths) {
|
|
935
|
+
console.log(` ${briefPath}`);
|
|
936
|
+
}
|
|
937
|
+
console.log("");
|
|
938
|
+
console.log(await formatDiscoverReport(process.cwd()));
|
|
939
|
+
} else if (sub === "discover") {
|
|
940
|
+
console.log(await formatDiscoverReport(process.cwd()));
|
|
941
|
+
} else if (sub === "promote") {
|
|
942
|
+
let scope = "feature";
|
|
943
|
+
let description = "";
|
|
944
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
945
|
+
const arg = rest[i];
|
|
946
|
+
if (arg.startsWith("--scope=")) {
|
|
947
|
+
scope = arg.slice("--scope=".length);
|
|
948
|
+
} else if (arg === "--scope" && rest[i + 1]) {
|
|
949
|
+
scope = rest[i + 1];
|
|
950
|
+
i += 1;
|
|
951
|
+
} else if (!arg.startsWith("--")) {
|
|
952
|
+
description = `${description} ${arg}`.trim();
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
console.log(formatPromoteMessage({ scope, description }));
|
|
956
|
+
} else {
|
|
957
|
+
throw new Error("Usage: req-analysis init | discover | promote");
|
|
958
|
+
}
|
|
959
|
+
} catch (err) {
|
|
960
|
+
console.error(`❌ ${err.message}`);
|
|
961
|
+
process.exit(1);
|
|
962
|
+
}
|
|
893
963
|
} else if (command === "classify-change") {
|
|
894
964
|
try {
|
|
895
965
|
let json = false;
|
package/lib/constants.js
CHANGED
|
@@ -77,6 +77,7 @@ export const SKILL_ASSETS = [
|
|
|
77
77
|
/** @type {{ file: string, remotePath: string }[]} */
|
|
78
78
|
export const REFERENCE_ASSETS = [
|
|
79
79
|
{ file: "explore.md", remotePath: "skills/references/explore.md" },
|
|
80
|
+
{ file: "elicitation.md", remotePath: "skills/references/elicitation.md" },
|
|
80
81
|
{ file: "solution-exploration.md", remotePath: "skills/references/solution-exploration.md" },
|
|
81
82
|
{ file: "project-init.md", remotePath: "skills/references/project-init.md" },
|
|
82
83
|
{ file: "constitution.md", remotePath: "skills/references/constitution.md" },
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { slugifyDescription } from "./feature.js";
|
|
5
|
+
import { ensureDir, writeFileSafe } from "./fs-utils.js";
|
|
6
|
+
import { NPX } from "./constants.js";
|
|
7
|
+
|
|
8
|
+
export const PROJECT_DIR = ".specs/project";
|
|
9
|
+
export const KICKOFF_FILENAME = "kickoff.md";
|
|
10
|
+
export const PROJECT_BRIEF_FILENAME = "requirements-brief.md";
|
|
11
|
+
export const FEATURE_BRIEFS_DIR = "feature-briefs";
|
|
12
|
+
|
|
13
|
+
/** @type {readonly string[]} */
|
|
14
|
+
export const DEFAULT_KICKOFF_DISCOVERY_PATHS = [
|
|
15
|
+
"prd.md",
|
|
16
|
+
"docs/brief.md",
|
|
17
|
+
"docs/prd.md",
|
|
18
|
+
path.join(PROJECT_DIR, KICKOFF_FILENAME),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} scope
|
|
23
|
+
* @returns {"project" | "feature"}
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeScope(scope) {
|
|
26
|
+
const value = (scope ?? "feature").toLowerCase();
|
|
27
|
+
if (value === "project" || value === "feature") {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
throw new Error('Scope must be "project" or "feature".');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {"project" | "feature"} scope
|
|
35
|
+
* @param {string} slug
|
|
36
|
+
* @param {string} [description]
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function briefPathForScope(scope, slug, description = "") {
|
|
40
|
+
if (scope === "project") {
|
|
41
|
+
return path.posix.join(PROJECT_DIR, PROJECT_BRIEF_FILENAME);
|
|
42
|
+
}
|
|
43
|
+
const featureSlug = slug || slugifyDescription(description || "feature");
|
|
44
|
+
return path.posix.join(
|
|
45
|
+
PROJECT_DIR,
|
|
46
|
+
FEATURE_BRIEFS_DIR,
|
|
47
|
+
featureSlug,
|
|
48
|
+
PROJECT_BRIEF_FILENAME,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} filePath
|
|
54
|
+
* @returns {Promise<boolean>}
|
|
55
|
+
*/
|
|
56
|
+
async function pathExists(filePath) {
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(filePath);
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} cwd
|
|
67
|
+
* @param {string[]} [extraPaths]
|
|
68
|
+
* @returns {Promise<Array<{ path: string, exists: boolean }>>}
|
|
69
|
+
*/
|
|
70
|
+
export async function discoverKickoffSources(cwd, extraPaths = []) {
|
|
71
|
+
const candidates = [...DEFAULT_KICKOFF_DISCOVERY_PATHS, ...extraPaths];
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
/** @type {Array<{ path: string, exists: boolean }>} */
|
|
74
|
+
const results = [];
|
|
75
|
+
|
|
76
|
+
for (const relative of candidates) {
|
|
77
|
+
const normalized = relative.replace(/\\/g, "/");
|
|
78
|
+
if (seen.has(normalized)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
seen.add(normalized);
|
|
82
|
+
const absolute = path.join(cwd, relative);
|
|
83
|
+
let exists = false;
|
|
84
|
+
try {
|
|
85
|
+
await fs.access(absolute);
|
|
86
|
+
exists = true;
|
|
87
|
+
} catch {
|
|
88
|
+
exists = false;
|
|
89
|
+
}
|
|
90
|
+
results.push({ path: normalized, exists });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {"project" | "feature"} scope
|
|
98
|
+
* @param {string} description
|
|
99
|
+
* @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
export function buildBriefTemplate(scope, description) {
|
|
102
|
+
const goal =
|
|
103
|
+
description.trim() || (scope === "project" ? "Project goal" : "Feature goal");
|
|
104
|
+
|
|
105
|
+
const featureSection =
|
|
106
|
+
scope === "project"
|
|
107
|
+
? `
|
|
108
|
+
## Feature candidates
|
|
109
|
+
|
|
110
|
+
| Suggested slug | Goal | Priority |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| 001-example | (one line) | P1 |
|
|
113
|
+
|
|
114
|
+
`
|
|
115
|
+
: "";
|
|
116
|
+
|
|
117
|
+
return `# Requirements brief: ${goal}
|
|
118
|
+
|
|
119
|
+
## Goal
|
|
120
|
+
|
|
121
|
+
${goal}
|
|
122
|
+
|
|
123
|
+
## Context sources
|
|
124
|
+
|
|
125
|
+
- (list every file or chat paste used)
|
|
126
|
+
|
|
127
|
+
## Current state
|
|
128
|
+
|
|
129
|
+
- (what exists in repo today — brownfield notes)
|
|
130
|
+
|
|
131
|
+
## Capabilities
|
|
132
|
+
|
|
133
|
+
- (product-language outcomes when done)
|
|
134
|
+
|
|
135
|
+
## Interaction details
|
|
136
|
+
|
|
137
|
+
- (UI screens/actions or API contracts — or "n/a")
|
|
138
|
+
|
|
139
|
+
## Constraints & out of scope
|
|
140
|
+
|
|
141
|
+
- In scope: …
|
|
142
|
+
- Out of scope: …
|
|
143
|
+
|
|
144
|
+
## Resolved questions
|
|
145
|
+
|
|
146
|
+
### D-001: (question one line)
|
|
147
|
+
|
|
148
|
+
- **Options considered**: A) … B) …
|
|
149
|
+
- **Decision**: …
|
|
150
|
+
- **Rationale**: …
|
|
151
|
+
- **Date**: YYYY-MM-DD
|
|
152
|
+
|
|
153
|
+
## Open questions
|
|
154
|
+
|
|
155
|
+
- none
|
|
156
|
+
|
|
157
|
+
${featureSection}## Owner approval
|
|
158
|
+
|
|
159
|
+
- Approved: yes
|
|
160
|
+
- Date: YYYY-MM-DD
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} description
|
|
166
|
+
* @param {{ scope?: string, cwd?: string, force?: boolean }} [options]
|
|
167
|
+
* @returns {Promise<{ scope: "project" | "feature", paths: string[], discovered: Array<{ path: string, exists: boolean }> }>}
|
|
168
|
+
*/
|
|
169
|
+
export async function reqAnalysisInit(description, options = {}) {
|
|
170
|
+
const cwd = options.cwd ?? process.cwd();
|
|
171
|
+
const scope = normalizeScope(options.scope ?? "feature");
|
|
172
|
+
const slug = scope === "feature" ? slugifyDescription(description || "feature") : "";
|
|
173
|
+
|
|
174
|
+
if (!description.trim() && scope === "feature") {
|
|
175
|
+
throw new Error(
|
|
176
|
+
'Description is required for feature scope. Example: req-analysis init "settings page" --scope feature',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const briefPath = briefPathForScope(scope, slug, description);
|
|
181
|
+
const kickoffRel = path.posix.join(PROJECT_DIR, KICKOFF_FILENAME);
|
|
182
|
+
const discovered = await discoverKickoffSources(cwd);
|
|
183
|
+
|
|
184
|
+
await ensureDir(path.join(cwd, ...path.dirname(briefPath).split("/")));
|
|
185
|
+
|
|
186
|
+
const briefFull = path.join(cwd, ...briefPath.split("/"));
|
|
187
|
+
const briefExists = await pathExists(briefFull);
|
|
188
|
+
if (briefExists && !options.force) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`${briefPath} already exists — use --force to replace the scaffold`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await writeFileSafe(briefFull, buildBriefTemplate(scope, description));
|
|
195
|
+
|
|
196
|
+
/** @type {string[]} */
|
|
197
|
+
const paths = [briefPath];
|
|
198
|
+
|
|
199
|
+
if (scope === "project") {
|
|
200
|
+
const kickoffFull = path.join(cwd, ...kickoffRel.split("/"));
|
|
201
|
+
const kickoffExists = await pathExists(kickoffFull);
|
|
202
|
+
if (!kickoffExists) {
|
|
203
|
+
await ensureDir(path.join(cwd, PROJECT_DIR));
|
|
204
|
+
await writeFileSafe(
|
|
205
|
+
kickoffFull,
|
|
206
|
+
`# Kickoff brief
|
|
207
|
+
|
|
208
|
+
(Paste product vision, goals, or export from your notes here.)
|
|
209
|
+
|
|
210
|
+
## Goal
|
|
211
|
+
|
|
212
|
+
${description.trim() || "(one sentence — what we are building)"}
|
|
213
|
+
|
|
214
|
+
## Notes
|
|
215
|
+
|
|
216
|
+
-
|
|
217
|
+
`,
|
|
218
|
+
);
|
|
219
|
+
paths.push(kickoffRel);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { scope, paths, discovered };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {{ scope?: string, briefPath?: string, description?: string }} [options]
|
|
228
|
+
* @returns {string}
|
|
229
|
+
*/
|
|
230
|
+
export function formatPromoteMessage(options = {}) {
|
|
231
|
+
const scope = normalizeScope(options.scope ?? "feature");
|
|
232
|
+
|
|
233
|
+
if (scope === "project") {
|
|
234
|
+
return [
|
|
235
|
+
"Project brief ready — next steps:",
|
|
236
|
+
" 1. Owner approves .specs/project/requirements-brief.md",
|
|
237
|
+
" 2. Update .specs/project/ROADMAP.md with feature candidates",
|
|
238
|
+
` 3. Pick a feature → ${NPX("req-analysis init \"…\" --scope feature")} or /specify`,
|
|
239
|
+
` 4. ${NPX("memory-index rebuild")} — index kickoff + brief`,
|
|
240
|
+
].join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const desc = options.description?.trim() || "feature description";
|
|
244
|
+
return [
|
|
245
|
+
"Feature brief ready — next steps:",
|
|
246
|
+
" 1. Owner approves the requirements brief",
|
|
247
|
+
` 2. ${NPX(`feature-init "${desc}"`)}`,
|
|
248
|
+
" 3. /specify — derive spec.md from brief (do not re-ask resolved questions)",
|
|
249
|
+
` 4. ${NPX("memory-index rebuild")}`,
|
|
250
|
+
].join("\n");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {string} cwd
|
|
255
|
+
* @returns {Promise<string>}
|
|
256
|
+
*/
|
|
257
|
+
export async function formatDiscoverReport(cwd) {
|
|
258
|
+
const discovered = await discoverKickoffSources(cwd);
|
|
259
|
+
const lines = ["Kickoff source discovery:", ""];
|
|
260
|
+
|
|
261
|
+
for (const entry of discovered) {
|
|
262
|
+
lines.push(` ${entry.exists ? "[found]" : "[missing]"} ${entry.path}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const found = discovered.filter((entry) => entry.exists);
|
|
266
|
+
if (found.length === 0) {
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push(
|
|
269
|
+
" No kickoff file yet — paste brief in chat or create .specs/project/kickoff.md",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luizsantiago/spec-guardrails",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Spec-driven process kit for AI coding agents: write goals in .specs/, break into tasks, implement in waves, verify with proof. Process mode (Node) or Brakes mode (Node + Python gates). Works with Cursor, Claude, Copilot, and Codex.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"guardrails": "node index.js",
|
|
14
14
|
"test": "npm run test:node && npm run test:gates",
|
|
15
|
-
"test:node": "node --test test/install.test.js test/test_feature_init.test.js test/test_config.test.js test/test_archive.test.js test/test_delta_merge.test.js test/test_presets.test.js test/test_brownfield.test.js test/test_doctor.test.js test/test_token_cost.test.js test/test_next_steps.test.js test/test_classify_change.test.js test/test_feature_status.test.js test/test_agent_contract.test.js test/test_gates_python.test.js 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.test.js test/test_sandbox_policy.test.js",
|
|
15
|
+
"test:node": "node --test test/install.test.js test/test_feature_init.test.js test/test_config.test.js test/test_archive.test.js test/test_delta_merge.test.js test/test_presets.test.js test/test_brownfield.test.js test/test_doctor.test.js test/test_token_cost.test.js test/test_next_steps.test.js test/test_classify_change.test.js test/test_feature_status.test.js test/test_agent_contract.test.js test/test_gates_python.test.js test/test_specs_utils.test.js 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.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
|
},
|
package/scripts/memory_index.py
CHANGED
|
@@ -41,6 +41,15 @@ SECTION_HEADING = re.compile(r"^##\s+(.+)$", re.MULTILINE)
|
|
|
41
41
|
|
|
42
42
|
LESSON_STATUSES = {"approved", "graduated", "confirmed"}
|
|
43
43
|
|
|
44
|
+
PROJECT_DIR = SPECS_DIR / "project"
|
|
45
|
+
KICKOFF_DISCOVERY_PATHS = (
|
|
46
|
+
Path("prd.md"),
|
|
47
|
+
Path("docs/brief.md"),
|
|
48
|
+
Path("docs/prd.md"),
|
|
49
|
+
PROJECT_DIR / "kickoff.md",
|
|
50
|
+
PROJECT_DIR / "requirements-brief.md",
|
|
51
|
+
)
|
|
52
|
+
|
|
44
53
|
|
|
45
54
|
def fail(message: str, code: int = EXIT_FAILED) -> int:
|
|
46
55
|
print(f"[{GATE}] FAIL - {DB_PATH}")
|
|
@@ -404,6 +413,58 @@ def index_lessons(conn: sqlite3.Connection, now: str) -> tuple[int, int]:
|
|
|
404
413
|
return entity_count, chunk_count
|
|
405
414
|
|
|
406
415
|
|
|
416
|
+
def index_kickoff_docs(conn: sqlite3.Connection, now: str) -> tuple[int, int]:
|
|
417
|
+
entity_count = 0
|
|
418
|
+
chunk_count = 0
|
|
419
|
+
seen: set[str] = set()
|
|
420
|
+
|
|
421
|
+
for rel_path in KICKOFF_DISCOVERY_PATHS:
|
|
422
|
+
path_key = str(rel_path).replace("\\", "/")
|
|
423
|
+
if path_key in seen:
|
|
424
|
+
continue
|
|
425
|
+
seen.add(path_key)
|
|
426
|
+
|
|
427
|
+
doc_path = Path(path_key)
|
|
428
|
+
if not doc_path.is_file():
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
entity_id = f"kickoff:{path_key.replace('/', ':')}"
|
|
432
|
+
label = path_key
|
|
433
|
+
text = doc_path.read_text(encoding="utf-8")
|
|
434
|
+
upsert_entity(conn, entity_id, "kickoff", label, path_key, now)
|
|
435
|
+
entity_count += 1
|
|
436
|
+
chunk_count += chunk_markdown_sections(
|
|
437
|
+
conn,
|
|
438
|
+
"project",
|
|
439
|
+
doc_path,
|
|
440
|
+
text,
|
|
441
|
+
"kickoff",
|
|
442
|
+
entity_id,
|
|
443
|
+
now,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
feature_briefs_root = PROJECT_DIR / "feature-briefs"
|
|
447
|
+
if feature_briefs_root.is_dir():
|
|
448
|
+
for brief_path in sorted(feature_briefs_root.rglob("requirements-brief.md")):
|
|
449
|
+
rel = brief_path.as_posix()
|
|
450
|
+
slug = brief_path.parent.name
|
|
451
|
+
entity_id = f"feature-brief:{slug}"
|
|
452
|
+
text = brief_path.read_text(encoding="utf-8")
|
|
453
|
+
upsert_entity(conn, entity_id, "feature-brief", slug, rel, now)
|
|
454
|
+
entity_count += 1
|
|
455
|
+
chunk_count += chunk_markdown_sections(
|
|
456
|
+
conn,
|
|
457
|
+
slug,
|
|
458
|
+
brief_path,
|
|
459
|
+
text,
|
|
460
|
+
"feature-brief",
|
|
461
|
+
entity_id,
|
|
462
|
+
now,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
return entity_count, chunk_count
|
|
466
|
+
|
|
467
|
+
|
|
407
468
|
def index_episodes(conn: sqlite3.Connection, now: str) -> tuple[int, int]:
|
|
408
469
|
episodes_path = SPECS_DIR / "state" / "episodes.json"
|
|
409
470
|
if not episodes_path.is_file():
|
|
@@ -586,6 +647,9 @@ def rebuild(json_output: bool = False) -> int:
|
|
|
586
647
|
conn, feature_id, tasks_path, tasks_text, now
|
|
587
648
|
)
|
|
588
649
|
|
|
650
|
+
kickoff_entities, kickoff_chunks = index_kickoff_docs(conn, now)
|
|
651
|
+
entity_count += kickoff_entities
|
|
652
|
+
chunk_count += kickoff_chunks
|
|
589
653
|
lesson_entities, lesson_chunks = index_lessons(conn, now)
|
|
590
654
|
entity_count += lesson_entities
|
|
591
655
|
chunk_count += lesson_chunks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-architecture
|
|
3
|
-
description: Spec-Driven Development hub for AI-assisted engineering. Progressive disclosure (~70% fewer skill tokens vs dumping the full kit). Adaptive phases with Python gates, independent verifier, discrimination sensor, evidence-or-zero, and .specs/ memory. Triggers on "specify feature", "design", "break into tasks", "implement", "verify", "quick fix", "resume work", "handoff".
|
|
3
|
+
description: Spec-Driven Development hub for AI-assisted engineering. Progressive disclosure (~70% fewer skill tokens vs dumping the full kit). Adaptive phases with Python gates, independent verifier, discrimination sensor, evidence-or-zero, and .specs/ memory. Triggers on "specify feature", "elicit", "design", "break into tasks", "implement", "verify", "quick fix", "resume work", "handoff".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Agent Architecture (Hub)
|
|
@@ -41,6 +41,7 @@ Structural gates run **before** owner review, so they cannot drift when the mode
|
|
|
41
41
|
| When | Command |
|
|
42
42
|
| --- | --- |
|
|
43
43
|
| Before `/specify` (Medium+) | `npx @luizsantiago/spec-guardrails feature-init "<description>"` (Tier 0) |
|
|
44
|
+
| Optional elicitation (vague kickoff or request) | `/elicit` → `req-analysis init` — **suggested only**, never blocks Specify |
|
|
44
45
|
| Optional project config | `init-config --preset node-ts` or `install --preset python` (see `preset list`) |
|
|
45
46
|
| Before confirming a spec | `python3 .specs/guardrails/scripts/validate_spec.py [feature]` |
|
|
46
47
|
| Before approving tasks | `python3 .specs/guardrails/scripts/analyze_artifacts.py [feature]` |
|
|
@@ -75,12 +76,13 @@ A **non-zero exit means STOP** — fix the artifact, then re-run the gate. Never
|
|
|
75
76
|
## Phase Map
|
|
76
77
|
|
|
77
78
|
```
|
|
78
|
-
EXPLORE (optional) → SPECIFY → DISCUSS (conditional) → DESIGN (optional) → TASKS (optional) → ANALYZE → EXECUTE (loop) → VERIFY → ARCHIVE
|
|
79
|
+
EXPLORE (optional) → ELICIT (optional) → SPECIFY → DISCUSS (conditional) → DESIGN (optional) → TASKS (optional) → ANALYZE → EXECUTE (loop) → VERIFY → ARCHIVE
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
| Phase | Required | Reference | Sister skill | Gate |
|
|
82
83
|
| --- | --- | --- | --- | --- |
|
|
83
84
|
| **Explore** | Optional | `references/explore.md` | — | — |
|
|
85
|
+
| **Elicit** | Optional | `references/elicitation.md` | — | — (v1 skill checklist; gate in 4.x wave 2) |
|
|
84
86
|
| **Constitution** | Once per project | `references/constitution.md` | — | — |
|
|
85
87
|
| **Specify** | Yes | `references/specify.md` | — | `validate_spec.py` |
|
|
86
88
|
| **Discuss** | Conditional | `references/discuss.md` | — | — |
|
|
@@ -131,6 +133,7 @@ Complexity determines depth. Do not run every phase on every change.
|
|
|
131
133
|
- **Design is skipped** when there are no architectural decisions and no new patterns.
|
|
132
134
|
- **Tasks is skipped** when there are ≤3 obvious steps.
|
|
133
135
|
- **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.
|
|
136
|
+
- **Elicit is suggested (never required)** when a kickoff brief exists without an approved project brief, or when the owner's request is vague ("add interface", "improve X") — see `references/elicitation.md`. If the owner chooses `/specify` directly, proceed.
|
|
134
137
|
- **Safety valve** — Even when Tasks is skipped, Execute starts by listing atomic steps inline. If that listing reveals more than 5 steps or real dependencies, STOP and create a formal `tasks.md`; the Tasks phase was skipped in error.
|
|
135
138
|
|
|
136
139
|
When in doubt, start at **Medium** and drop phases only with owner approval.
|
|
@@ -145,6 +148,9 @@ When in doubt, start at **Medium** and drop phases only with owner approval.
|
|
|
145
148
|
| `.specs/project/PROJECT.md` | Vision, stack, constraints (when the project defines them) |
|
|
146
149
|
| `.specs/project/CONSTITUTION.md` | Governing principles (when Constitution ran) |
|
|
147
150
|
| `.specs/project/ROADMAP.md` | Milestones and feature status |
|
|
151
|
+
| `.specs/project/kickoff.md` | Owner kickoff brief (paste or file — optional) |
|
|
152
|
+
| `.specs/project/requirements-brief.md` | Project-level elicitation output (when `/elicit` project ran) |
|
|
153
|
+
| `.specs/project/feature-briefs/[slug]/requirements-brief.md` | Feature-level elicitation (when `/elicit` feature ran) |
|
|
148
154
|
| `.specs/config.yaml` | Optional project context and per-phase rules |
|
|
149
155
|
| `.specs/domains/[domain]/spec.md` | Long-lived domain truth after Archive |
|
|
150
156
|
| `.specs/quick/NNN-slug/` | Quick-mode tasks and summaries |
|
|
@@ -212,6 +218,7 @@ Project rules: `.cursor/rules/engineering-baseline.mdc` (always applied in Curso
|
|
|
212
218
|
| Command | Reference | Action |
|
|
213
219
|
| --- | --- | --- |
|
|
214
220
|
| `/explore` | `references/explore.md` | Think through ideas before Specify |
|
|
221
|
+
| `/elicit` | `references/elicitation.md` | Structured Q&A before Specify — project or feature scope |
|
|
215
222
|
| `/project-init` | `references/project-init.md` | Brownfield: map repo → PROJECT + domain stubs |
|
|
216
223
|
| `/constitution` | `references/constitution.md` | Create project governing principles |
|
|
217
224
|
| `/specify` | `references/specify.md` | `feature-init` then requirements; EARS; delta specs |
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Elicitation
|
|
2
|
+
|
|
3
|
+
Structured requirements discovery **before** `/specify`. Reads kickoff briefs, the local repo, and owner answers — then writes a **requirements brief** the Specify phase formalizes into `spec.md`.
|
|
4
|
+
|
|
5
|
+
Chat command: **`/elicit`**. CLI scaffold: `req-analysis init`.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
**Project mode** (`--scope project`):
|
|
10
|
+
|
|
11
|
+
- After `install`, when a kickoff brief exists (file or pasted into chat)
|
|
12
|
+
- Brownfield after `project-init` — enrich PROJECT + ROADMAP before first feature
|
|
13
|
+
- Owner asks to "analyze the project" or "read my PRD and ask questions"
|
|
14
|
+
|
|
15
|
+
**Feature mode** (`--scope feature`):
|
|
16
|
+
|
|
17
|
+
- A specific delivery is named but details are missing ("add a settings page", "improve login")
|
|
18
|
+
- A ROADMAP candidate needs detail before `/specify`
|
|
19
|
+
- `/specify` would otherwise need many `[NEEDS CLARIFICATION]` markers
|
|
20
|
+
|
|
21
|
+
**Suggest, never block.** If the owner prefers `/specify` directly, proceed — Elicitation is optional.
|
|
22
|
+
|
|
23
|
+
## When NOT to Use
|
|
24
|
+
|
|
25
|
+
- **Quick tier** — use `quick-mode.md`
|
|
26
|
+
- **Explore** — owner still choosing *what* to build (options A/B/C), not *how*
|
|
27
|
+
- **Spec-ready request** — testable goal, clear scope → `/specify` directly
|
|
28
|
+
- **Discuss** — gray irreversible decision *inside* an existing spec draft → `discuss.md`
|
|
29
|
+
|
|
30
|
+
## Phase boundaries (do not repeat other phases)
|
|
31
|
+
|
|
32
|
+
| Phase | Asks about | Output |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| **Explore** | What to build — alternatives | none |
|
|
35
|
+
| **Elicit (project)** | Gaps in kickoff + macro scope | `.specs/project/requirements-brief.md` |
|
|
36
|
+
| **Elicit (feature)** | Gaps for one delivery | `.specs/project/feature-briefs/[slug]/requirements-brief.md` |
|
|
37
|
+
| **Specify** | Formal REQ/EARS — minimal questions | `spec.md` |
|
|
38
|
+
| **Discuss** | Irreversible gray areas mid-spec | `context.md` |
|
|
39
|
+
|
|
40
|
+
Before each question: **cite the source** — "kickoff §Goal already says X — confirm?" or "not found — A) … B) …".
|
|
41
|
+
|
|
42
|
+
## Inputs
|
|
43
|
+
|
|
44
|
+
- Kickoff brief — any of:
|
|
45
|
+
- `.specs/project/kickoff.md` (pasted chat or saved export)
|
|
46
|
+
- `prd.md`, `docs/brief.md`, `docs/prd.md` (discovered automatically)
|
|
47
|
+
- Owner paste in chat → persist to `.specs/project/kickoff.md` on first turn
|
|
48
|
+
- `.specs/project/PROJECT.md`, `ROADMAP.md` when present
|
|
49
|
+
- Relevant code (`code-index search`, targeted reads) for brownfield
|
|
50
|
+
- `memory-retrieve` after `memory-index rebuild` for prior briefs
|
|
51
|
+
- `context-limits.md` — one scope at a time
|
|
52
|
+
|
|
53
|
+
**Not in v1:** dedicated GitHub or ChatPRD CLI. If the owner has external docs, they paste export or point to a local path.
|
|
54
|
+
|
|
55
|
+
## Output
|
|
56
|
+
|
|
57
|
+
| Scope | Primary artifact | Also updates |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| **project** | `.specs/project/requirements-brief.md` | ROADMAP feature candidates (suggested slugs) |
|
|
60
|
+
| **feature** | `.specs/project/feature-briefs/[slug]/requirements-brief.md` | — |
|
|
61
|
+
|
|
62
|
+
Optional scaffold:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx @luizsantiago/spec-guardrails req-analysis init "description" --scope project
|
|
66
|
+
npx @luizsantiago/spec-guardrails req-analysis init "settings page" --scope feature
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Procedure
|
|
70
|
+
|
|
71
|
+
### 0. Choose scope
|
|
72
|
+
|
|
73
|
+
| Signal | Scope |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| First project pass, kickoff/PRD present, no project brief yet | **project** |
|
|
76
|
+
| Named feature, vague details, or ROADMAP item | **feature** |
|
|
77
|
+
| Owner unsure | Ask once; default **feature** for a single ask, **project** for greenfield kickoff |
|
|
78
|
+
|
|
79
|
+
Run `req-analysis init` when templates help; otherwise create the brief path directly.
|
|
80
|
+
|
|
81
|
+
### 1. Gather context (read-only)
|
|
82
|
+
|
|
83
|
+
1. List **Context sources** — every file read (paths + section if partial).
|
|
84
|
+
2. Run `req-analysis discover` (or read kickoff paths manually): `prd.md`, `docs/brief.md`, `.specs/project/kickoff.md`.
|
|
85
|
+
3. Skim PROJECT.md, ROADMAP, brownfield code if relevant.
|
|
86
|
+
4. `code-index search "<keywords>"` when the repo already has code.
|
|
87
|
+
5. `memory-retrieve "<topic>"` when memory index exists.
|
|
88
|
+
|
|
89
|
+
Summarize **Current state** in the brief — do not copy entire kickoff verbatim.
|
|
90
|
+
|
|
91
|
+
### 2. Gap analysis
|
|
92
|
+
|
|
93
|
+
List gaps only — requirements the kickoff + repo **do not** answer:
|
|
94
|
+
|
|
95
|
+
- UI: screens, buttons, actions, empty/loading/error
|
|
96
|
+
- API: endpoints, auth, errors
|
|
97
|
+
- Data: entities, retention
|
|
98
|
+
- Scope: in / out for this pass
|
|
99
|
+
|
|
100
|
+
Mark covered items: `covered by kickoff.md §…` — **do not ask again**.
|
|
101
|
+
|
|
102
|
+
### 3. Elicitation rounds
|
|
103
|
+
|
|
104
|
+
| Rule | Value |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| Max questions per round | **5** |
|
|
107
|
+
| Format | Concrete options A/B/C + recommendation (same as `discuss.md`) |
|
|
108
|
+
| One area per round | UI **or** API **or** data — not all at once |
|
|
109
|
+
| Stop when | Gaps closed, owner says "enough", or Open questions is `- none` |
|
|
110
|
+
|
|
111
|
+
Templates by detected type (ask only relevant dimensions):
|
|
112
|
+
|
|
113
|
+
- **UI** — layout, primary actions, navigation, states
|
|
114
|
+
- **API** — contract, auth, errors, idempotency
|
|
115
|
+
- **Data** — entities, migrations, retention
|
|
116
|
+
- **CLI/infra** — flags, envs, rollback
|
|
117
|
+
|
|
118
|
+
No production code. No `spec.md` until `/specify`.
|
|
119
|
+
|
|
120
|
+
### 4. Write the brief
|
|
121
|
+
|
|
122
|
+
Use the template from `req-analysis init`. Required sections:
|
|
123
|
+
|
|
124
|
+
- **Goal** — one sentence
|
|
125
|
+
- **Context sources**
|
|
126
|
+
- **Current state**
|
|
127
|
+
- **Capabilities** — product language, not EARS
|
|
128
|
+
- **Interaction details** — when UI/API applies
|
|
129
|
+
- **Constraints & out of scope**
|
|
130
|
+
- **Resolved questions** — D-001 format (from `discuss.md`)
|
|
131
|
+
- **Open questions** — `- none` when done
|
|
132
|
+
- **Feature candidates** (project mode) — suggested NNN-slug + one-line goal
|
|
133
|
+
- **Owner approval** — date + explicit yes
|
|
134
|
+
|
|
135
|
+
### 5. Owner approval
|
|
136
|
+
|
|
137
|
+
Present the brief summary. Wait for explicit approval before `/specify`.
|
|
138
|
+
|
|
139
|
+
**v1 checklist (no Python gate yet):**
|
|
140
|
+
|
|
141
|
+
- [ ] Open questions is `- none` or empty
|
|
142
|
+
- [ ] Owner approval filled
|
|
143
|
+
- [ ] Context sources lists at least one input
|
|
144
|
+
- [ ] No `[NEEDS CLARIFICATION]` or `[OPEN QUESTION]` left
|
|
145
|
+
|
|
146
|
+
### 6. Transition
|
|
147
|
+
|
|
148
|
+
**Project mode:**
|
|
149
|
+
|
|
150
|
+
1. Update ROADMAP with feature candidates (stubs — no spec.md yet).
|
|
151
|
+
2. Owner picks next feature → **feature mode** if still vague, else `/specify`.
|
|
152
|
+
|
|
153
|
+
**Feature mode:**
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npx @luizsantiago/spec-guardrails feature-init "owner description"
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Copy or link brief → `.specs/features/NNN-slug/requirements-brief.md` (optional).
|
|
160
|
+
|
|
161
|
+
Open `specify.md` — derive `spec.md` from brief; do **not** re-ask resolved questions.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx @luizsantiago/spec-guardrails req-analysis promote --scope feature
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Prints promote steps when unsure.
|
|
168
|
+
|
|
169
|
+
## Router (hub — suggest only)
|
|
170
|
+
|
|
171
|
+
When the owner message is vague (interface without flow, "improve X" without criteria, kickoff exists but no `requirements-brief.md`):
|
|
172
|
+
|
|
173
|
+
> Suggest: "Want `/elicit` (a few targeted questions) or go straight to `/specify`?"
|
|
174
|
+
|
|
175
|
+
Never refuse `/specify`. Never run Elicitation without owner consent when they chose Specify.
|
|
176
|
+
|
|
177
|
+
## Rules
|
|
178
|
+
|
|
179
|
+
- English artifacts (see `engineering-standards.md`).
|
|
180
|
+
- Complement kickoff — do not rewrite or replace the owner's product brief.
|
|
181
|
+
- Lazy artifacts — create brief only when Elicitation runs.
|
|
182
|
+
- Re-run `memory-index rebuild` after approving a project brief so kickoff + brief are searchable.
|
|
183
|
+
|
|
184
|
+
## Anti-Patterns
|
|
185
|
+
|
|
186
|
+
| Avoid | Prefer |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| 80 generic questions | ≤5 per round until gaps close |
|
|
189
|
+
| Re-asking kickoff content | Cite source + confirm |
|
|
190
|
+
| Writing spec.md here | Brief in product language; Specify formalizes |
|
|
191
|
+
| Duplicating Discuss | Irreversible grays wait for Discuss inside Specify |
|
|
192
|
+
|
|
193
|
+
## Next
|
|
194
|
+
|
|
195
|
+
- Brief approved, feature chosen → `specify.md`
|
|
196
|
+
- Still choosing what to build → `explore.md`
|
|
197
|
+
- Back → `agent-architecture.md`
|
|
@@ -57,5 +57,6 @@ None required. Optionally capture decisions in chat. When the idea crystallizes,
|
|
|
57
57
|
## Next
|
|
58
58
|
|
|
59
59
|
- Scope is clear → `feature-init` then `specify.md`
|
|
60
|
+
- Details missing but direction chosen → `elicitation.md` (feature scope)
|
|
60
61
|
- Gray areas remain → `discuss.md` inside Specify
|
|
61
62
|
- Back → `agent-architecture.md`
|
|
@@ -14,6 +14,7 @@ Capture WHAT to build as testable, traceable requirements. Always required (exce
|
|
|
14
14
|
## Inputs
|
|
15
15
|
|
|
16
16
|
- Owner's request, in their own words
|
|
17
|
+
- `.specs/project/requirements-brief.md` or `.specs/project/feature-briefs/*/requirements-brief.md` when `/elicit` ran — do not re-ask resolved questions
|
|
17
18
|
- `.specs/STATE.md` decisions (`AD-NNN`) relevant to this area
|
|
18
19
|
- `.specs/project/CONSTITUTION.md` when present
|
|
19
20
|
- `.specs/LESSONS.md` entries that apply — load them with `python3 .specs/guardrails/scripts/lessons.py list --status confirmed`
|
|
@@ -63,6 +63,15 @@ memory:
|
|
|
63
63
|
semantic_weight: 0.4
|
|
64
64
|
graph_depth: 1
|
|
65
65
|
|
|
66
|
+
# Elicitation (/elicit — optional, before Specify)
|
|
67
|
+
elicitation:
|
|
68
|
+
max_questions_per_turn: 5
|
|
69
|
+
kickoff_discovery_paths:
|
|
70
|
+
- prd.md
|
|
71
|
+
- docs/brief.md
|
|
72
|
+
- docs/prd.md
|
|
73
|
+
- .specs/project/kickoff.md
|
|
74
|
+
|
|
66
75
|
# Soft OS sandbox (optional — policy, not containers)
|
|
67
76
|
sandbox:
|
|
68
77
|
mode: warn
|