@esneiderbravo/speclaw 1.0.1 → 2.0.1
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 +76 -59
- package/dist/cli/commands/cortex.js +49 -0
- package/dist/cli/commands/init.js +23 -16
- package/dist/cli/commands/lawbook.js +34 -1
- package/dist/cli/commands/update.js +66 -58
- package/dist/cli/index.js +12 -3
- package/dist/cli/lib/update-check.js +8 -5
- 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/workflows/speclaw.yml +1 -1
- package/dist/modules/foundation/context-budget.js +2 -0
- package/dist/modules/foundation/setup-tool.js +3 -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
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { parseFlags } from "./lib/args.js";
|
|
3
3
|
import { ui, header } from "./lib/ui.js";
|
|
4
4
|
import { maybeNotifyUpdate } from "./lib/update-check.js";
|
|
5
|
-
const HELP = `speclaw — spec-driven, agent-ready projects (
|
|
5
|
+
const HELP = `speclaw — spec-driven, agent-ready projects (Foundation + Compass + Lawbook + Cortex)
|
|
6
6
|
|
|
7
7
|
Usage: speclaw <command> [options]
|
|
8
8
|
|
|
@@ -12,8 +12,8 @@ Install globally so the command is always available:
|
|
|
12
12
|
Setup
|
|
13
13
|
init Interactive setup: pick agents, scaffold, index, get the prompt
|
|
14
14
|
(--minimal omits setup/lifecycle MCP tools)
|
|
15
|
-
update
|
|
16
|
-
(--
|
|
15
|
+
update Apply project migrations (advisory if a newer binary exists)
|
|
16
|
+
(--check reports version only; --minimal persists minimal exposure)
|
|
17
17
|
agent list Show which agents are configured
|
|
18
18
|
agent add <id> Configure another agent later (symlinks + MCP)
|
|
19
19
|
|
|
@@ -31,6 +31,9 @@ Compass (code intelligence — the same surface agents use via MCP)
|
|
|
31
31
|
trace <from> <to> A call path between two nodes
|
|
32
32
|
visualize [node] Interactive HTML graph → .speclaw/graph.html
|
|
33
33
|
|
|
34
|
+
Cortex (One brain. Many agents. — multi-agent loop)
|
|
35
|
+
cortex <op> status|start|advance|rework|brief — drive harness.json (--change)
|
|
36
|
+
|
|
34
37
|
Lawbook (spec-driven workflow)
|
|
35
38
|
quick <name> Scaffold a level-0 change (record.md + reports)
|
|
36
39
|
lawbook init Create the lawbook/ workspace
|
|
@@ -41,6 +44,7 @@ Lawbook (spec-driven workflow)
|
|
|
41
44
|
lawbook validate <c> Validate a change's artifacts
|
|
42
45
|
lawbook sync <c> Promote delta specs to canonical
|
|
43
46
|
lawbook archive <c> Finalize and archive a change
|
|
47
|
+
lawbook harness <op> Deprecated alias for \`speclaw cortex\` (compat)
|
|
44
48
|
|
|
45
49
|
Other
|
|
46
50
|
doctor Verify the installation (--json, --offline, --strict)
|
|
@@ -84,6 +88,7 @@ const HEADER_COMMANDS = new Set([
|
|
|
84
88
|
"index",
|
|
85
89
|
"watch",
|
|
86
90
|
"lawbook",
|
|
91
|
+
"cortex",
|
|
87
92
|
"quick",
|
|
88
93
|
]);
|
|
89
94
|
/**
|
|
@@ -116,6 +121,8 @@ function maybeHeader(cmd, flags) {
|
|
|
116
121
|
return;
|
|
117
122
|
if (cmd === "lawbook" && flags.json && flags._[0] === "draft")
|
|
118
123
|
return;
|
|
124
|
+
if (cmd === "cortex" && flags.json)
|
|
125
|
+
return;
|
|
119
126
|
header();
|
|
120
127
|
}
|
|
121
128
|
/** Run the handler for a single command. Returns when the command completes. */
|
|
@@ -160,6 +167,8 @@ async function dispatch(cmd, flags) {
|
|
|
160
167
|
return (await import("./commands/visualize.js")).runVisualize(flags);
|
|
161
168
|
case "quick":
|
|
162
169
|
return (await import("./commands/quick.js")).runQuick(flags);
|
|
170
|
+
case "cortex":
|
|
171
|
+
return (await import("./commands/cortex.js")).runCortex(flags);
|
|
163
172
|
case "lawbook":
|
|
164
173
|
return (await import("./commands/lawbook.js")).runSpec(flags);
|
|
165
174
|
case "doctor":
|
|
@@ -102,15 +102,16 @@ export function npmPackageUrl(name) {
|
|
|
102
102
|
/**
|
|
103
103
|
* Build the two-line "update available" notice. The latest version is rendered
|
|
104
104
|
* as a clickable link to the package's npm page, so a capable terminal lets the
|
|
105
|
-
* user open the release with a single click
|
|
106
|
-
*
|
|
105
|
+
* user open the release with a single click. Project migrations stay
|
|
106
|
+
* `speclaw update`; the binary is upgraded separately (prefer npx).
|
|
107
107
|
*
|
|
108
108
|
* @param current - The installed version.
|
|
109
109
|
* @param latest - The newest published version.
|
|
110
110
|
* @returns The formatted, styled notice (no leading/trailing blank lines).
|
|
111
111
|
*/
|
|
112
112
|
export function upgradeNotice(current, latest) {
|
|
113
|
-
const
|
|
113
|
+
const name = pkgName();
|
|
114
|
+
const latestLink = c.cyan(link(latest, npmPackageUrl(name)));
|
|
114
115
|
return (" " +
|
|
115
116
|
c.amber("⬆ speclaw ") +
|
|
116
117
|
c.muted(current + " → ") +
|
|
@@ -118,9 +119,11 @@ export function upgradeNotice(current, latest) {
|
|
|
118
119
|
c.muted(" available") +
|
|
119
120
|
"\n" +
|
|
120
121
|
" " +
|
|
121
|
-
c.muted("
|
|
122
|
+
c.muted("prefer ") +
|
|
123
|
+
c.cyan(`npx ${name}@latest update`) +
|
|
124
|
+
c.muted(" — or ") +
|
|
122
125
|
c.cyan("speclaw update") +
|
|
123
|
-
c.muted("
|
|
126
|
+
c.muted(" to migrate this project"));
|
|
124
127
|
}
|
|
125
128
|
/**
|
|
126
129
|
* Print the "update available" notice to stderr when a newer version exists.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage → role brief for Cortex coordinators.
|
|
3
|
+
*
|
|
4
|
+
* // Covers: req~cortex-module~1
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Map a harness stage to the role the coordinator should dispatch.
|
|
8
|
+
*
|
|
9
|
+
* @param stage - Current harness stage, or null when not started.
|
|
10
|
+
*/
|
|
11
|
+
export function briefForStage(stage) {
|
|
12
|
+
switch (stage) {
|
|
13
|
+
case "exploring":
|
|
14
|
+
return {
|
|
15
|
+
role: "explorer",
|
|
16
|
+
agentPath: "ai-specs/agents/explorer.md",
|
|
17
|
+
skillHints: ["explore"],
|
|
18
|
+
nextOps: ["advance"],
|
|
19
|
+
};
|
|
20
|
+
case "planning":
|
|
21
|
+
return {
|
|
22
|
+
role: "planner",
|
|
23
|
+
agentPath: "ai-specs/agents/planner.md",
|
|
24
|
+
skillHints: ["draft"],
|
|
25
|
+
nextOps: ["advance", "pauseForQuestions"],
|
|
26
|
+
};
|
|
27
|
+
case "questions":
|
|
28
|
+
return {
|
|
29
|
+
role: "planner",
|
|
30
|
+
agentPath: "ai-specs/agents/planner.md",
|
|
31
|
+
skillHints: ["draft"],
|
|
32
|
+
nextOps: ["advance"],
|
|
33
|
+
};
|
|
34
|
+
case "implementing":
|
|
35
|
+
return {
|
|
36
|
+
role: "implementer",
|
|
37
|
+
agentPath: "ai-specs/agents/implementer.md",
|
|
38
|
+
skillHints: ["build"],
|
|
39
|
+
nextOps: ["advance"],
|
|
40
|
+
};
|
|
41
|
+
case "reviewing":
|
|
42
|
+
return {
|
|
43
|
+
role: "reviewer",
|
|
44
|
+
agentPath: "ai-specs/agents/reviewer.md",
|
|
45
|
+
skillHints: [],
|
|
46
|
+
nextOps: ["advance", "rework"],
|
|
47
|
+
};
|
|
48
|
+
case "testing":
|
|
49
|
+
return {
|
|
50
|
+
role: "tester",
|
|
51
|
+
agentPath: "ai-specs/agents/tester.md",
|
|
52
|
+
skillHints: ["test"],
|
|
53
|
+
nextOps: ["advance", "rework"],
|
|
54
|
+
};
|
|
55
|
+
case "archiving":
|
|
56
|
+
return {
|
|
57
|
+
role: "archiver",
|
|
58
|
+
agentPath: "ai-specs/agents/archiver.md",
|
|
59
|
+
skillHints: ["sync", "archive"],
|
|
60
|
+
nextOps: ["advance"],
|
|
61
|
+
};
|
|
62
|
+
case "done":
|
|
63
|
+
case null:
|
|
64
|
+
return {
|
|
65
|
+
role: null,
|
|
66
|
+
agentPath: null,
|
|
67
|
+
skillHints: [],
|
|
68
|
+
nextOps: [],
|
|
69
|
+
};
|
|
70
|
+
default: {
|
|
71
|
+
const _exhaustive = stage;
|
|
72
|
+
throw new Error(`unknown stage ${String(_exhaustive)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -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
|
|