@esneiderbravo/speclaw 1.0.0 → 2.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 +70 -56
- package/dist/cli/commands/cortex.js +49 -0
- package/dist/cli/commands/init.js +21 -15
- package/dist/cli/commands/lawbook.js +34 -1
- package/dist/cli/commands/update.js +25 -2
- package/dist/cli/index.js +10 -1
- package/dist/modules/cortex/brief.js +75 -0
- package/dist/modules/cortex/harness.js +270 -0
- package/dist/modules/cortex/register.js +49 -0
- package/dist/modules/foundation/assets/AGENTS.template.md +13 -6
- package/dist/modules/foundation/assets/CLAUDE.template.md +15 -11
- package/dist/modules/foundation/assets/docs/standards/lawbook.template.md +24 -37
- package/dist/modules/foundation/assets/laws/laws-manifest.json +16 -7
- package/dist/modules/foundation/assets/workflows/speclaw.yml +1 -1
- package/dist/modules/foundation/compile-laws.js +5 -7
- package/dist/modules/foundation/context-budget.js +2 -0
- package/dist/modules/foundation/graph.js +7 -3
- package/dist/modules/foundation/laws.js +30 -32
- package/dist/modules/foundation/scaffold.js +9 -6
- package/dist/modules/foundation/seed-laws.js +263 -0
- package/dist/modules/foundation/setup-tool.js +3 -2
- package/dist/modules/foundation/verify.js +2 -2
- package/dist/modules/lawbook/assets/agents/archiver.md +19 -0
- package/dist/modules/lawbook/assets/agents/explorer.md +24 -0
- package/dist/modules/lawbook/assets/agents/implementer.md +20 -0
- package/dist/modules/lawbook/assets/agents/planner.md +27 -0
- package/dist/modules/lawbook/assets/agents/reviewer.md +25 -0
- package/dist/modules/lawbook/assets/agents/tester.md +21 -0
- package/dist/modules/lawbook/assets/commands/cortex.md +11 -0
- package/dist/modules/lawbook/assets/rules/spec-tasks-mandatory-steps.md +11 -10
- package/dist/modules/lawbook/assets/skills/build/steps/03-implement.md +1 -1
- package/dist/modules/lawbook/assets/skills/build/steps/04-hand-off.md +17 -0
- package/dist/modules/lawbook/assets/skills/cortex/SKILL.md +13 -0
- package/dist/modules/lawbook/assets/skills/cortex/steps/01-load-or-start.md +25 -0
- package/dist/modules/lawbook/assets/skills/cortex/steps/02-dispatch-loop.md +19 -0
- package/dist/modules/lawbook/assets/skills/cortex/steps/03-complete.md +10 -0
- package/dist/modules/lawbook/assets/skills/test/SKILL.md +11 -0
- package/dist/modules/lawbook/assets/skills/{build/steps/04-quality-gates.md → test/steps/01-quality-gates.md} +1 -1
- package/dist/modules/lawbook/assets/skills/{build/steps/05-manual-verification.md → test/steps/02-manual-verification.md} +1 -1
- package/dist/modules/lawbook/assets/skills/{build/steps/06-discipline-reports.md → test/steps/03-discipline-reports.md} +5 -2
- package/dist/modules/lawbook/change-tool.js +22 -0
- package/dist/modules/lawbook/engine.js +4 -0
- package/dist/modules/lawbook/levels.js +9 -3
- package/dist/modules/lawbook/register.js +7 -0
- package/dist/modules/tools/assets/packs.json +1 -6
- package/dist/server.js +4 -1
- package/dist/shared/agents.js +3 -3
- package/dist/shared/exposure.js +2 -2
- package/dist/shared/tool-catalog.js +3 -2
- package/package.json +2 -2
- package/dist/modules/lawbook/assets/skills/build/steps/07-hand-off.md +0 -9
- package/dist/modules/tools/assets/packs/agents/backend-developer.md +0 -61
- package/dist/modules/tools/assets/packs/agents/frontend-developer.md +0 -62
- package/dist/modules/tools/assets/packs/agents/product-strategy-analyst.md +0 -56
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic multi-agent harness state for a lawbook change.
|
|
3
|
+
*
|
|
4
|
+
* Speclaw does not spawn LLMs. This module only persists stage transitions;
|
|
5
|
+
* the host agent (Cursor/Claude/…) follows the Cortex skill and calls these
|
|
6
|
+
* ops via `speclaw cortex` or the `cortex` MCP tool.
|
|
7
|
+
*
|
|
8
|
+
* Cortex must not import from the lawbook module (no cycles). Ceremony level
|
|
9
|
+
* is read locally from `change.json` `confirmedLevel` (default 3).
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
export const HARNESS_STAGES = [
|
|
14
|
+
"exploring",
|
|
15
|
+
"planning",
|
|
16
|
+
"questions",
|
|
17
|
+
"implementing",
|
|
18
|
+
"reviewing",
|
|
19
|
+
"testing",
|
|
20
|
+
"archiving",
|
|
21
|
+
"done",
|
|
22
|
+
];
|
|
23
|
+
export const harnessOps = ["status", "start", "advance", "rework"];
|
|
24
|
+
const DEFAULT_MAX_REWORK = 3;
|
|
25
|
+
function specRoot(projectPath) {
|
|
26
|
+
return path.join(projectPath, "lawbook");
|
|
27
|
+
}
|
|
28
|
+
function changeDir(projectPath, change) {
|
|
29
|
+
return path.join(specRoot(projectPath), "changes", change);
|
|
30
|
+
}
|
|
31
|
+
function harnessPath(projectPath, change) {
|
|
32
|
+
return path.join(changeDir(projectPath, change), "harness.json");
|
|
33
|
+
}
|
|
34
|
+
function nowIso() {
|
|
35
|
+
return new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
function requireChangeDir(projectPath, change) {
|
|
38
|
+
const dir = changeDir(projectPath, change);
|
|
39
|
+
if (!fs.existsSync(dir)) {
|
|
40
|
+
throw new Error(`change "${change}" not found under lawbook/changes/`);
|
|
41
|
+
}
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read confirmed ceremony level from change.json without importing lawbook.
|
|
46
|
+
* Missing file or field → 3.
|
|
47
|
+
*/
|
|
48
|
+
function readConfirmedLevel(projectPath, change) {
|
|
49
|
+
const p = path.join(changeDir(projectPath, change), "change.json");
|
|
50
|
+
if (!fs.existsSync(p))
|
|
51
|
+
return 3;
|
|
52
|
+
try {
|
|
53
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
54
|
+
const n = raw.confirmedLevel;
|
|
55
|
+
if (n === 0 || n === 1 || n === 2 || n === 3)
|
|
56
|
+
return n;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* default */
|
|
60
|
+
}
|
|
61
|
+
return 3;
|
|
62
|
+
}
|
|
63
|
+
/** Read harness.json or return null when absent. */
|
|
64
|
+
export function readHarness(projectPath, change) {
|
|
65
|
+
requireChangeDir(projectPath, change);
|
|
66
|
+
const p = harnessPath(projectPath, change);
|
|
67
|
+
if (!fs.existsSync(p))
|
|
68
|
+
return null;
|
|
69
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
70
|
+
}
|
|
71
|
+
function writeHarness(projectPath, change, state) {
|
|
72
|
+
const p = harnessPath(projectPath, change);
|
|
73
|
+
fs.writeFileSync(p, JSON.stringify(state, null, 2) + "\n");
|
|
74
|
+
}
|
|
75
|
+
function nextAfterExplore(level) {
|
|
76
|
+
return level === 0 ? "implementing" : "planning";
|
|
77
|
+
}
|
|
78
|
+
function nextAfterImplement(level) {
|
|
79
|
+
return level === 0 ? "testing" : "reviewing";
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Legal one-step advances (not rework). Rework is handled separately.
|
|
83
|
+
*/
|
|
84
|
+
function allowedAdvance(from, level, verdict) {
|
|
85
|
+
switch (from) {
|
|
86
|
+
case "exploring":
|
|
87
|
+
return nextAfterExplore(level);
|
|
88
|
+
case "planning":
|
|
89
|
+
return "implementing";
|
|
90
|
+
case "questions":
|
|
91
|
+
return "planning";
|
|
92
|
+
case "implementing":
|
|
93
|
+
return nextAfterImplement(level);
|
|
94
|
+
case "reviewing":
|
|
95
|
+
if (verdict === "FAIL") {
|
|
96
|
+
throw new Error(`harness advance from reviewing with FAIL requires op "rework" (not advance)`);
|
|
97
|
+
}
|
|
98
|
+
if (verdict !== "PASS") {
|
|
99
|
+
throw new Error(`harness advance from reviewing requires verdict PASS or FAIL`);
|
|
100
|
+
}
|
|
101
|
+
return "testing";
|
|
102
|
+
case "testing":
|
|
103
|
+
if (verdict === "FAIL") {
|
|
104
|
+
throw new Error(`harness advance from testing with FAIL requires op "rework"`);
|
|
105
|
+
}
|
|
106
|
+
if (verdict !== "PASS") {
|
|
107
|
+
throw new Error(`harness advance from testing requires verdict PASS or FAIL`);
|
|
108
|
+
}
|
|
109
|
+
return "archiving";
|
|
110
|
+
case "archiving":
|
|
111
|
+
return "done";
|
|
112
|
+
case "done":
|
|
113
|
+
throw new Error(`harness already done — no further advance`);
|
|
114
|
+
default: {
|
|
115
|
+
const _exhaustive = from;
|
|
116
|
+
throw new Error(`unknown stage ${String(_exhaustive)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Dispatch a harness op for a change.
|
|
122
|
+
*
|
|
123
|
+
* // Covers: req~harness-state~1
|
|
124
|
+
*/
|
|
125
|
+
export function handleHarness(args) {
|
|
126
|
+
const { projectPath, change, harnessOp } = args;
|
|
127
|
+
requireChangeDir(projectPath, change);
|
|
128
|
+
if (harnessOp === "status") {
|
|
129
|
+
return { state: readHarness(projectPath, change) };
|
|
130
|
+
}
|
|
131
|
+
if (harnessOp === "start") {
|
|
132
|
+
const existing = readHarness(projectPath, change);
|
|
133
|
+
if (existing && existing.stage !== "done") {
|
|
134
|
+
throw new Error(`harness already started for "${change}" (stage=${existing.stage}); use status/advance/rework`);
|
|
135
|
+
}
|
|
136
|
+
const level = readConfirmedLevel(projectPath, change);
|
|
137
|
+
const state = {
|
|
138
|
+
version: 1,
|
|
139
|
+
change,
|
|
140
|
+
stage: "exploring",
|
|
141
|
+
level,
|
|
142
|
+
iteration: 0,
|
|
143
|
+
maxRework: DEFAULT_MAX_REWORK,
|
|
144
|
+
verdicts: { review: null, test: null },
|
|
145
|
+
openQuestions: [],
|
|
146
|
+
history: [
|
|
147
|
+
{
|
|
148
|
+
at: nowIso(),
|
|
149
|
+
from: "exploring",
|
|
150
|
+
to: "exploring",
|
|
151
|
+
op: "start",
|
|
152
|
+
note: args.note,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
writeHarness(projectPath, change, state);
|
|
157
|
+
return state;
|
|
158
|
+
}
|
|
159
|
+
const current = readHarness(projectPath, change);
|
|
160
|
+
if (!current) {
|
|
161
|
+
throw new Error(`no harness.json for "${change}" — run cortex start first`);
|
|
162
|
+
}
|
|
163
|
+
if (harnessOp === "advance") {
|
|
164
|
+
// Planner may pause for questions instead of advancing to implement.
|
|
165
|
+
if (current.stage === "planning" && args.pauseForQuestions) {
|
|
166
|
+
const questions = args.openQuestions ?? [];
|
|
167
|
+
if (questions.length === 0) {
|
|
168
|
+
throw new Error(`pauseForQuestions requires at least one openQuestions entry`);
|
|
169
|
+
}
|
|
170
|
+
const next = {
|
|
171
|
+
...current,
|
|
172
|
+
stage: "questions",
|
|
173
|
+
openQuestions: questions,
|
|
174
|
+
history: [
|
|
175
|
+
...current.history,
|
|
176
|
+
{
|
|
177
|
+
at: nowIso(),
|
|
178
|
+
from: "planning",
|
|
179
|
+
to: "questions",
|
|
180
|
+
op: "advance",
|
|
181
|
+
note: args.note,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
writeHarness(projectPath, change, next);
|
|
186
|
+
return next;
|
|
187
|
+
}
|
|
188
|
+
const to = allowedAdvance(current.stage, current.level, args.verdict ?? undefined);
|
|
189
|
+
const verdicts = { ...current.verdicts };
|
|
190
|
+
if (current.stage === "reviewing" && args.verdict)
|
|
191
|
+
verdicts.review = args.verdict;
|
|
192
|
+
if (current.stage === "testing" && args.verdict)
|
|
193
|
+
verdicts.test = args.verdict;
|
|
194
|
+
const next = {
|
|
195
|
+
...current,
|
|
196
|
+
stage: to,
|
|
197
|
+
verdicts,
|
|
198
|
+
openQuestions: current.stage === "questions" ? [] : current.openQuestions,
|
|
199
|
+
history: [
|
|
200
|
+
...current.history,
|
|
201
|
+
{
|
|
202
|
+
at: nowIso(),
|
|
203
|
+
from: current.stage,
|
|
204
|
+
to,
|
|
205
|
+
op: "advance",
|
|
206
|
+
note: args.note,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
writeHarness(projectPath, change, next);
|
|
211
|
+
return next;
|
|
212
|
+
}
|
|
213
|
+
if (harnessOp === "rework") {
|
|
214
|
+
if (current.stage !== "reviewing" && current.stage !== "testing") {
|
|
215
|
+
throw new Error(`rework only allowed from reviewing or testing (now ${current.stage})`);
|
|
216
|
+
}
|
|
217
|
+
if (args.verdict !== "FAIL") {
|
|
218
|
+
throw new Error(`rework requires verdict FAIL`);
|
|
219
|
+
}
|
|
220
|
+
if (current.iteration >= current.maxRework) {
|
|
221
|
+
throw new Error(`max rework (${current.maxRework}) reached — coordinator must ask the human before continuing`);
|
|
222
|
+
}
|
|
223
|
+
const verdicts = { ...current.verdicts };
|
|
224
|
+
if (current.stage === "reviewing")
|
|
225
|
+
verdicts.review = "FAIL";
|
|
226
|
+
if (current.stage === "testing")
|
|
227
|
+
verdicts.test = "FAIL";
|
|
228
|
+
const next = {
|
|
229
|
+
...current,
|
|
230
|
+
stage: "implementing",
|
|
231
|
+
iteration: current.iteration + 1,
|
|
232
|
+
verdicts,
|
|
233
|
+
history: [
|
|
234
|
+
...current.history,
|
|
235
|
+
{
|
|
236
|
+
at: nowIso(),
|
|
237
|
+
from: current.stage,
|
|
238
|
+
to: "implementing",
|
|
239
|
+
op: "rework",
|
|
240
|
+
note: args.note,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
writeHarness(projectPath, change, next);
|
|
245
|
+
return next;
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`unknown harnessOp '${String(harnessOp)}'`);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Archive blockers derived from harness verdicts.
|
|
251
|
+
*
|
|
252
|
+
* // Covers: req~harness-archive-gate~1
|
|
253
|
+
*/
|
|
254
|
+
export function harnessArchiveBlockers(projectPath, change) {
|
|
255
|
+
const state = readHarness(projectPath, change);
|
|
256
|
+
if (!state) {
|
|
257
|
+
return [`missing harness.json — run \`speclaw cortex start --change ${change}\``];
|
|
258
|
+
}
|
|
259
|
+
const blockers = [];
|
|
260
|
+
if (state.verdicts.test !== "PASS") {
|
|
261
|
+
blockers.push(`harness test verdict is not PASS (got ${String(state.verdicts.test)})`);
|
|
262
|
+
}
|
|
263
|
+
if (state.level >= 1 && state.verdicts.review !== "PASS") {
|
|
264
|
+
blockers.push(`harness review verdict is not PASS (got ${String(state.verdicts.review)})`);
|
|
265
|
+
}
|
|
266
|
+
if (state.stage !== "archiving" && state.stage !== "done") {
|
|
267
|
+
blockers.push(`harness stage is ${state.stage} — advance to archiving before archive`);
|
|
268
|
+
}
|
|
269
|
+
return blockers;
|
|
270
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cortex MCP registration — One brain. Many agents.
|
|
3
|
+
*
|
|
4
|
+
* // Covers: req~cortex-module~1
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { defineTool, text } from "../../shared/mcp.js";
|
|
8
|
+
import { shouldExpose } from "../../shared/exposure.js";
|
|
9
|
+
import { handleHarness, harnessOps, readHarness, } from "./harness.js";
|
|
10
|
+
import { briefForStage } from "./brief.js";
|
|
11
|
+
export const cortexActions = [...harnessOps, "brief"];
|
|
12
|
+
/**
|
|
13
|
+
* Register the canonical `cortex` MCP tool.
|
|
14
|
+
*
|
|
15
|
+
* @param server - MCP server instance.
|
|
16
|
+
* @param opts - Exposure profile.
|
|
17
|
+
*/
|
|
18
|
+
export function registerCortex(server, opts = {}) {
|
|
19
|
+
const minimal = Boolean(opts.minimal);
|
|
20
|
+
const add = (name, description, inputSchema, handler) => {
|
|
21
|
+
if (!shouldExpose(name, minimal))
|
|
22
|
+
return;
|
|
23
|
+
defineTool(server, { name, description, inputSchema, handler });
|
|
24
|
+
};
|
|
25
|
+
add("cortex", "Cortex multi-agent loop: status, start, advance, rework, brief for a change.", {
|
|
26
|
+
projectPath: z.string(),
|
|
27
|
+
change: z.string(),
|
|
28
|
+
action: z.enum(cortexActions),
|
|
29
|
+
note: z.string().optional(),
|
|
30
|
+
verdict: z.enum(["PASS", "FAIL"]).optional(),
|
|
31
|
+
openQuestions: z.array(z.string()).optional(),
|
|
32
|
+
pauseForQuestions: z.boolean().optional(),
|
|
33
|
+
}, async ({ projectPath, change, action, note, verdict, openQuestions, pauseForQuestions }) => {
|
|
34
|
+
if (action === "brief") {
|
|
35
|
+
const state = readHarness(projectPath, change);
|
|
36
|
+
const brief = briefForStage(state?.stage ?? null);
|
|
37
|
+
return text(JSON.stringify({ state, ...brief }, null, 2));
|
|
38
|
+
}
|
|
39
|
+
return text(handleHarness({
|
|
40
|
+
projectPath,
|
|
41
|
+
change,
|
|
42
|
+
harnessOp: action,
|
|
43
|
+
note,
|
|
44
|
+
verdict: verdict ?? null,
|
|
45
|
+
openQuestions,
|
|
46
|
+
pauseForQuestions,
|
|
47
|
+
}));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -22,23 +22,30 @@ Claude-specific notes: [`CLAUDE.md`](CLAUDE.md). The law: [`LAWS.md`](LAWS.md).
|
|
|
22
22
|
after Compass returns nothing useful, the graph is missing (`compass_index`
|
|
23
23
|
first), or the target isn't indexed code (stylesheets, config, logs). Cheat
|
|
24
24
|
sheet: [`docs/compass.md`](docs/compass.md).
|
|
25
|
-
3. **Follow
|
|
26
|
-
within the same PR.
|
|
25
|
+
3. **Follow Cortex** (*One brain. Many agents.*) for every non-trivial change;
|
|
26
|
+
archive within the same PR. The primary agent is the **coordinator**
|
|
27
|
+
(`cortex` skill / `/lawbook/cortex`) — it MUST NOT implement
|
|
28
|
+
product code itself. It dispatches explorer → planner → implementer →
|
|
29
|
+
reviewer → tester → archiver (or adopts each role while obeying that
|
|
30
|
+
role's permission profile). Drive state with the `cortex` MCP tool or
|
|
31
|
+
`speclaw cortex`. Rules:
|
|
27
32
|
[`docs/standards/lawbook.md`](docs/standards/lawbook.md).
|
|
28
33
|
4. **Run the quality gates yourself** before declaring anything done — see
|
|
29
|
-
[`docs/standards/testing-standards.md`](docs/standards/testing-standards.md)
|
|
34
|
+
[`docs/standards/testing-standards.md`](docs/standards/testing-standards.md)
|
|
35
|
+
(owned by the **tester** role under Cortex):
|
|
30
36
|
- Tests: `{{test_commands}}`
|
|
31
37
|
- Lint / type-check: `{{lint_commands}}`
|
|
32
38
|
5. **Respect the conventions** — branches `{{branch_pattern}}`, commits
|
|
33
39
|
{{commit_style}}, code that reads like its neighbors. See
|
|
34
40
|
[`docs/standards/base-standards.md`](docs/standards/base-standards.md) and
|
|
35
41
|
[`docs/standards/conventions.md`](docs/standards/conventions.md).
|
|
36
|
-
6. **Use the skills.** `ai-specs/` is the canonical home for
|
|
37
|
-
and
|
|
42
|
+
6. **Use the skills and role agents.** `ai-specs/` is the canonical home for
|
|
43
|
+
skills, commands, rules, and agents, mirrored to each IDE directory via
|
|
44
|
+
symlinks.
|
|
38
45
|
7. **Ask before irreversible or outward-facing actions** — destructive commands;
|
|
39
46
|
writing to a real data store (DB rows or files with real user data, including
|
|
40
47
|
for tests — verify against an isolated/throwaway store); publishing
|
|
41
|
-
reviews/tickets/comments.
|
|
48
|
+
reviews/tickets/comments. Planner questions always go to the human.
|
|
42
49
|
|
|
43
50
|
## The standards (the law, in detail)
|
|
44
51
|
|
|
@@ -48,21 +48,24 @@ target is not indexed code — stylesheets, JSON/config, markdown, logs,
|
|
|
48
48
|
generated files, lockfiles. Never skip Compass because grep "feels faster."
|
|
49
49
|
See [`docs/compass.md`](docs/compass.md).
|
|
50
50
|
|
|
51
|
-
## Rule 2 — Spec-driven, always
|
|
51
|
+
## Rule 2 — Spec-driven Cortex loop, always
|
|
52
52
|
|
|
53
|
-
No non-trivial change lands without a
|
|
54
|
-
verify → archive).
|
|
53
|
+
No non-trivial change lands without a lawbook change (propose → implement →
|
|
54
|
+
verify → archive) run through **Cortex** (*One brain. Many agents.*). You are the
|
|
55
|
+
**coordinator** unless you were spawned as a role agent: use the `cortex`
|
|
56
|
+
skill / `/lawbook/cortex` and the `cortex` MCP tool; do **not** implement product code yourself.
|
|
57
|
+
Role agents live in `ai-specs/agents/`. The rules are in
|
|
55
58
|
[`docs/standards/lawbook.md`](docs/standards/lawbook.md);
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
workflow skills live in `ai-specs/skills/` and `/lawbook` commands wrap them.
|
|
60
|
+
A change is not done until it is archived — archiving belongs in the PR.
|
|
58
61
|
|
|
59
62
|
## Rule 3 — Quality gates are non-negotiable
|
|
60
63
|
|
|
61
64
|
- Tests: `{{test_commands}}`
|
|
62
65
|
- Lint / type-check: `{{lint_commands}}`
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
observe. Full rules:
|
|
67
|
+
The **tester** role runs them and reports real output. Never claim success you
|
|
68
|
+
did not observe. Full rules:
|
|
66
69
|
[`docs/standards/testing-standards.md`](docs/standards/testing-standards.md).
|
|
67
70
|
|
|
68
71
|
## Rule 4 — Conventions
|
|
@@ -72,11 +75,11 @@ its neighbors, comments that carry constraints (never ticket IDs). Full rules:
|
|
|
72
75
|
[`docs/standards/base-standards.md`](docs/standards/base-standards.md) and
|
|
73
76
|
[`docs/standards/conventions.md`](docs/standards/conventions.md).
|
|
74
77
|
|
|
75
|
-
## Rule 5 — Skills are law-adjacent
|
|
78
|
+
## Rule 5 — Skills and role agents are law-adjacent
|
|
76
79
|
|
|
77
|
-
Skills, commands, and
|
|
78
|
-
`.claude/`, `.cursor/`, `.codex/`, `.agents/`). When a skill matches
|
|
79
|
-
task, use it — do not improvise a parallel process.
|
|
80
|
+
Skills, commands, rules, and agents live in `ai-specs/` (symlinked into
|
|
81
|
+
`.claude/`, `.cursor/`, `.codex/`, `.agents/`). When a skill or role matches
|
|
82
|
+
the task, use it — do not improvise a parallel process.
|
|
80
83
|
|
|
81
84
|
## Rule 6 — Stop conditions
|
|
82
85
|
|
|
@@ -85,3 +88,4 @@ schema drops), writing to a real data store (DB rows or files holding real user
|
|
|
85
88
|
data — including to set up or tear down test data; verification runs against an
|
|
86
89
|
isolated/throwaway store instead), publishing anything outward-facing (PR
|
|
87
90
|
reviews, tickets, comments), or any action that contradicts a standard.
|
|
91
|
+
Planner clarifying questions always go to the human via the coordinator.
|
|
@@ -6,38 +6,35 @@ mechanical steps are speclaw MCP tools).
|
|
|
6
6
|
|
|
7
7
|
## The loop
|
|
8
8
|
|
|
9
|
-
No non-trivial change lands without a
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
No non-trivial change lands without a lawbook change. The default execution
|
|
10
|
+
model is **Cortex** (*One brain. Many agents.*): the host primary agent is the
|
|
11
|
+
**coordinator** (`cortex` skill / MCP `cortex`) and dispatches explorer → planner →
|
|
12
|
+
implementer → reviewer → tester → archiver. State:
|
|
13
|
+
`lawbook/changes/<name>/harness.json` (`speclaw cortex`). Lawbook owns specs
|
|
14
|
+
and ceremony; Cortex owns the multi-agent loop.
|
|
15
|
+
|
|
16
|
+
1. **explore** (explorer) — think an idea through before committing (writes nothing).
|
|
17
|
+
2. **draft** / **quick** (planner) — create `lawbook/changes/<name>/` artifacts
|
|
18
|
+
for the confirmed ceremony level; questions go to the human via the
|
|
19
|
+
coordinator.
|
|
20
|
+
3. **build** (implementer) — implement tasks; hand off before final gates.
|
|
21
|
+
4. **review** (reviewer) — `reports/review.md` PASS/FAIL (skipped at level 0).
|
|
22
|
+
5. **test** (tester) — quality gates, manual verification, discipline reports.
|
|
23
|
+
6. **sync** / **archive** (archiver) — reconcile, sync when needed, archive
|
|
24
|
+
within the same PR. Gated on Cortex verdicts plus tasks/reports/sync.
|
|
23
25
|
|
|
24
26
|
## Mandatory task steps
|
|
25
27
|
|
|
26
28
|
`tasks.md` MUST include the steps defined in `lawbook/config.yaml` and the
|
|
27
|
-
`spec-tasks-mandatory-steps` rule
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
itself — never delegates it.
|
|
29
|
+
`spec-tasks-mandatory-steps` rule. The **tester** role performs manual
|
|
30
|
+
verification — never the user; the coordinator must not archive without a
|
|
31
|
+
test PASS.
|
|
31
32
|
|
|
32
33
|
## Reports
|
|
33
34
|
|
|
34
|
-
Every change carries a `reports/` folder.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
whenever the change touches an API surface) recording what was tested
|
|
38
|
-
and the real results — unit, integration, and end-to-end as applicable — with
|
|
39
|
-
the commands run and their output. It is evidence of testing that travels with
|
|
40
|
-
the change; the archive is blocked until at least one discipline report exists.
|
|
35
|
+
Every change carries a `reports/` folder. The tester writes discipline reports;
|
|
36
|
+
the reviewer writes `review.md`. Archive is blocked until evidence and harness
|
|
37
|
+
verdicts are complete.
|
|
41
38
|
|
|
42
39
|
## Delta specs
|
|
43
40
|
|
|
@@ -51,18 +48,8 @@ the change; the archive is blocked until at least one discipline report exists.
|
|
|
51
48
|
## Archiving discipline
|
|
52
49
|
|
|
53
50
|
Always archive with the `archive` command / `lawbook_archive` tool, never a manual
|
|
54
|
-
`mv
|
|
55
|
-
|
|
56
|
-
Before archiving, the agent runs a reconciliation review: it compares what was
|
|
57
|
-
built against the delta specs and, when the code has drifted past the original
|
|
58
|
-
contracts, shows short insights and reconciles the delta specs.
|
|
59
|
-
|
|
60
|
-
`lawbook_archive` is then **gated in the engine** — it refuses to archive (and
|
|
61
|
-
reports the reason) while any task is unchecked, while `reports/` holds no
|
|
62
|
-
discipline report, or while the delta specs are not yet synced into the
|
|
63
|
-
canonical specs. Because the gate covers both the tool and the CLI, a change
|
|
64
|
-
reaches the archive only when it is genuinely complete: reconcile, `sync`, then
|
|
65
|
-
archive.
|
|
51
|
+
`mv`. The engine refuses archive while tasks are unchecked, reports are missing,
|
|
52
|
+
specs are out of sync when required, or harness review/test verdicts are not PASS.
|
|
66
53
|
|
|
67
54
|
## Amendments to the law
|
|
68
55
|
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"prose": "Justify any new dependency in the PR: it must not break 'runs offline with no API keys'.",
|
|
22
22
|
"verification": { "kind": "path" },
|
|
23
23
|
"enforcement": "feedback",
|
|
24
|
-
"source": { "file": "LAWS.md" }
|
|
24
|
+
"source": { "file": "LAWS.md" },
|
|
25
|
+
"requires": ["src/modules/compass", "src/modules/foundation"]
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
28
|
"id": "law~protect-templates~1",
|
|
@@ -32,7 +33,8 @@
|
|
|
32
33
|
"prose": "Assets under src/modules/*/assets/** are product output — keep the {{placeholder}} and speclaw-init contracts intact, and let the build copy them (never hand-copy into dist/).",
|
|
33
34
|
"verification": { "kind": "path" },
|
|
34
35
|
"enforcement": "feedback",
|
|
35
|
-
"source": { "file": "LAWS.md" }
|
|
36
|
+
"source": { "file": "LAWS.md" },
|
|
37
|
+
"requires": ["src/modules/foundation/assets"]
|
|
36
38
|
},
|
|
37
39
|
{
|
|
38
40
|
"id": "law~honest-attribution~1",
|
|
@@ -43,7 +45,8 @@
|
|
|
43
45
|
"prose": "Keep ATTRIBUTION.md accurate as the Compass and Lawbook modules evolve.",
|
|
44
46
|
"verification": { "kind": "path" },
|
|
45
47
|
"enforcement": "feedback",
|
|
46
|
-
"source": { "file": "LAWS.md" }
|
|
48
|
+
"source": { "file": "LAWS.md" },
|
|
49
|
+
"requires": ["ATTRIBUTION.md"]
|
|
47
50
|
},
|
|
48
51
|
{
|
|
49
52
|
"id": "law~shared-stays-inner~1",
|
|
@@ -62,7 +65,8 @@
|
|
|
62
65
|
}
|
|
63
66
|
},
|
|
64
67
|
"enforcement": "gate",
|
|
65
|
-
"source": { "file": "docs/standards/architecture.md" }
|
|
68
|
+
"source": { "file": "docs/standards/architecture.md" },
|
|
69
|
+
"requires": ["src/shared", "src/modules", "src/cli"]
|
|
66
70
|
},
|
|
67
71
|
{
|
|
68
72
|
"id": "law~compass-does-not-import-foundation~1",
|
|
@@ -81,7 +85,8 @@
|
|
|
81
85
|
}
|
|
82
86
|
},
|
|
83
87
|
"enforcement": "gate",
|
|
84
|
-
"source": { "file": "docs/standards/architecture.md" }
|
|
88
|
+
"source": { "file": "docs/standards/architecture.md" },
|
|
89
|
+
"requires": ["src/modules/compass", "src/modules/foundation"]
|
|
85
90
|
},
|
|
86
91
|
{
|
|
87
92
|
"id": "law~no-module-cycles~1",
|
|
@@ -90,9 +95,13 @@
|
|
|
90
95
|
"severity": "error",
|
|
91
96
|
"scope": ["src/modules/**"],
|
|
92
97
|
"prose": "There are no circular dependencies between modules.",
|
|
93
|
-
"verification": {
|
|
98
|
+
"verification": {
|
|
99
|
+
"kind": "graph",
|
|
100
|
+
"rule": { "circular": true, "edgeKinds": ["import"] }
|
|
101
|
+
},
|
|
94
102
|
"enforcement": "gate",
|
|
95
|
-
"source": { "file": "docs/standards/architecture.md" }
|
|
103
|
+
"source": { "file": "docs/standards/architecture.md" },
|
|
104
|
+
"adaptScope": "source-roots"
|
|
96
105
|
}
|
|
97
106
|
]
|
|
98
107
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { globError, isActiveLaw, readLawManifest,
|
|
3
|
+
import { globError, isActiveLaw, readLawManifest, seedManifestFor, mergeSeedLaws, writeLawManifest, } from "./laws.js";
|
|
4
4
|
import { parseLawsFromStandards } from "./laws-parse.js";
|
|
5
5
|
import { agentsmdDialect, claudeRulesDialect, coderabbitDialect, copilotDialect, cursorMdcDialect, patchDelimited, } from "./dialects/index.js";
|
|
6
6
|
import { detectConfiguredAgents } from "../../shared/agents.js";
|
|
@@ -17,6 +17,7 @@ const DIALECTS = [
|
|
|
17
17
|
* Throws if standards declare duplicate ids.
|
|
18
18
|
*/
|
|
19
19
|
export function mergeLawSources(projectPath) {
|
|
20
|
+
// Covers: req~adapt-seed-to-repo~1
|
|
20
21
|
const parsed = parseLawsFromStandards(projectPath);
|
|
21
22
|
if (parsed.duplicates.size > 0) {
|
|
22
23
|
const detail = [...parsed.duplicates.entries()]
|
|
@@ -24,18 +25,15 @@ export function mergeLawSources(projectPath) {
|
|
|
24
25
|
.join("; ");
|
|
25
26
|
throw new Error(`duplicate law id(s) in docs/standards: ${detail}`);
|
|
26
27
|
}
|
|
28
|
+
const disk = readLawManifest(projectPath);
|
|
29
|
+
const base = disk ? mergeSeedLaws(disk, projectPath).manifest : seedManifestFor(projectPath);
|
|
27
30
|
const byId = new Map();
|
|
28
|
-
for (const law of
|
|
31
|
+
for (const law of base.laws)
|
|
29
32
|
byId.set(law.id, law);
|
|
30
33
|
for (const law of parsed.laws) {
|
|
31
34
|
if (!byId.has(law.id))
|
|
32
35
|
byId.set(law.id, law);
|
|
33
36
|
}
|
|
34
|
-
const disk = readLawManifest(projectPath);
|
|
35
|
-
if (disk) {
|
|
36
|
-
for (const law of disk.laws)
|
|
37
|
-
byId.set(law.id, law);
|
|
38
|
-
}
|
|
39
37
|
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
40
38
|
}
|
|
41
39
|
function validateScopes(laws) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerCompass } from "../compass/register.js";
|
|
2
|
+
import { registerCortex } from "../cortex/register.js";
|
|
2
3
|
import { registerSpec } from "../lawbook/register.js";
|
|
3
4
|
import { registerTools } from "../tools/register.js";
|
|
4
5
|
import { registerFoundationCore } from "./register-core.js";
|
|
@@ -25,6 +26,7 @@ export function collectRegisteredTools(minimal) {
|
|
|
25
26
|
registerFoundationCore(stub, opts);
|
|
26
27
|
registerSpec(stub, opts);
|
|
27
28
|
registerCompass(stub, opts);
|
|
29
|
+
registerCortex(stub, opts);
|
|
28
30
|
registerTools(stub, opts);
|
|
29
31
|
return tools;
|
|
30
32
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { matchesScope } from "./laws.js";
|
|
1
2
|
import { underPaths } from "./verify-model.js";
|
|
2
|
-
/** Build the cross-file dependency graph, restricted to `paths`
|
|
3
|
-
function buildGraph(db, paths, edgeKinds) {
|
|
3
|
+
/** Build the cross-file dependency graph, restricted to `paths` and `scope`. */
|
|
4
|
+
function buildGraph(db, paths, edgeKinds, scope) {
|
|
4
5
|
const kindFilter = edgeKinds && edgeKinds.length > 0
|
|
5
6
|
? ` AND e.kind IN (${edgeKinds.map(() => "?").join(", ")})`
|
|
6
7
|
: "";
|
|
@@ -16,6 +17,8 @@ function buildGraph(db, paths, edgeKinds) {
|
|
|
16
17
|
for (const { src, dst } of rows) {
|
|
17
18
|
if (!underPaths(src, paths) || !underPaths(dst, paths))
|
|
18
19
|
continue;
|
|
20
|
+
if (!matchesScope(scope, src) || !matchesScope(scope, dst))
|
|
21
|
+
continue;
|
|
19
22
|
const list = adj.get(src);
|
|
20
23
|
if (list)
|
|
21
24
|
list.push(dst);
|
|
@@ -205,8 +208,9 @@ function reachableFindings(law, rule, adj) {
|
|
|
205
208
|
* resolved graph, so a graph law never reports an unknown here).
|
|
206
209
|
*/
|
|
207
210
|
export function runGraphLaw(db, law, paths) {
|
|
211
|
+
// Covers: req~graph-honours-scope~1
|
|
208
212
|
const rule = law.verification.rule;
|
|
209
|
-
const adj = buildGraph(db, paths, rule.edgeKinds);
|
|
213
|
+
const adj = buildGraph(db, paths, rule.edgeKinds, law.scope);
|
|
210
214
|
const findings = [];
|
|
211
215
|
const wantReachable = rule.reachable === true && rule.from != null && rule.to != null;
|
|
212
216
|
const wantCircular = rule.circular === true || (!rule.circular && !wantReachable);
|