@esneiderbravo/speclaw 2.0.3 → 2.0.5
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/dist/cli/commands/check.js +11 -1
- package/dist/cli/commands/cortex.js +17 -0
- package/dist/cli/commands/index-build.js +13 -2
- package/dist/cli/commands/lawbook.js +31 -10
- package/dist/cli/commands/query.js +13 -0
- package/dist/cli/index.js +4 -1
- package/dist/modules/compass/indexer.js +14 -0
- package/dist/modules/compass/register.js +17 -1
- package/dist/modules/cortex/compass-gate.js +84 -0
- package/dist/modules/cortex/harness.js +20 -11
- package/dist/modules/cortex/types.js +14 -0
- package/dist/modules/foundation/assets/AGENTS.template.md +2 -2
- package/dist/modules/foundation/assets/CLAUDE.template.md +4 -3
- package/dist/modules/foundation/assets/docs/compass.template.md +7 -7
- package/dist/modules/foundation/assets/docs/standards/lawbook.template.md +2 -2
- package/dist/modules/foundation/check.js +23 -5
- package/dist/modules/foundation/compass-nudge.js +189 -0
- package/dist/modules/foundation/hooks.js +18 -3
- package/dist/modules/foundation/integrity.js +20 -7
- package/dist/modules/foundation/lock.js +24 -7
- package/dist/modules/lawbook/assets/agents/archiver.md +1 -1
- package/dist/modules/lawbook/assets/agents/explorer.md +7 -4
- package/dist/modules/lawbook/assets/agents/implementer.md +1 -1
- package/dist/modules/lawbook/assets/agents/planner.md +7 -4
- package/dist/modules/lawbook/assets/agents/reviewer.md +1 -1
- package/dist/modules/lawbook/assets/agents/tester.md +1 -1
- package/dist/modules/lawbook/assets/commands/draft.md +2 -2
- package/dist/modules/lawbook/assets/commands/quick.md +2 -2
- package/dist/modules/lawbook/assets/rules/spec-reports-disciplines.md +1 -1
- package/dist/modules/lawbook/assets/rules/spec-tasks-mandatory-steps.md +4 -4
- package/dist/modules/lawbook/assets/skills/archive/steps/02-reconcile.md +1 -1
- package/dist/modules/lawbook/assets/skills/build/steps/01-load-change.md +1 -1
- package/dist/modules/lawbook/assets/skills/cortex/steps/02-dispatch-loop.md +22 -6
- package/dist/modules/lawbook/assets/skills/draft/steps/01-ensure-workspace.md +1 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +7 -4
- package/dist/modules/lawbook/assets/skills/draft/steps/03-name-capabilities.md +3 -1
- package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +12 -2
- package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +15 -6
- package/dist/modules/lawbook/assets/skills/explore/steps/02-summarize.md +2 -0
- package/dist/modules/lawbook/assets/skills/investigate/steps/02-hand-off.md +1 -1
- package/dist/modules/lawbook/assets/skills/quick/steps/02-implement.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/steps/01-confirm.md +1 -1
- package/dist/modules/lawbook/assets/skills/sync/steps/02-reconcile.md +1 -1
- package/dist/modules/lawbook/bugfix.js +22 -26
- package/dist/modules/lawbook/change-tool.js +15 -0
- package/dist/modules/lawbook/engine.js +11 -0
- package/dist/modules/lawbook/levels.js +2 -0
- package/dist/modules/lawbook/quick.js +14 -23
- package/dist/modules/lawbook/register.js +1 -1
- package/dist/modules/lawbook/scaffold-change.js +238 -0
- package/dist/shared/compass-calls.js +197 -0
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { NUDGE_ENTRY, isEvidenceTool, readCompassCalls, recordCompassCall, } from "../../shared/compass-calls.js";
|
|
4
|
+
// Pure extension → language table: loads no grammar and opens no index DB.
|
|
5
|
+
import { langForPath } from "../compass/languages.js";
|
|
6
|
+
// Covers: req~compass-nudge~1
|
|
7
|
+
//
|
|
8
|
+
// The Compass-first nudge: on `PostToolUse` for Read/Grep/Glob of indexed code,
|
|
9
|
+
// remind the agent to locate and read through Compass when the call log shows
|
|
10
|
+
// no recent Compass evidence. Advisory context only — it never gates and never
|
|
11
|
+
// touches the verdict. It reads only the bounded call-log tail and the
|
|
12
|
+
// in-memory extension table, so it stays inside the hook latency budget.
|
|
13
|
+
/** Tools whose completed calls the nudge watches. */
|
|
14
|
+
export const NUDGE_TOOLS = new Set(["Read", "Grep", "Glob"]);
|
|
15
|
+
/** No nudge when a Compass evidence call landed within this window. */
|
|
16
|
+
export const NUDGE_EVIDENCE_WINDOW_MS = 10 * 60 * 1000;
|
|
17
|
+
/** At most one nudge per this window. */
|
|
18
|
+
export const NUDGE_RATE_LIMIT_MS = 5 * 60 * 1000;
|
|
19
|
+
/** Path segments that are never indexed source. */
|
|
20
|
+
const SKIP_SEGMENTS = new Set(["node_modules", ".git", ".speclaw", "dist"]);
|
|
21
|
+
/**
|
|
22
|
+
* True for a `PostToolUse` Read/Grep/Glob call. Those calls are the nudge's
|
|
23
|
+
* alone: laws govern mutations, so `checkAction` skips law evaluation for them.
|
|
24
|
+
*/
|
|
25
|
+
export function isNudgeEvent(args) {
|
|
26
|
+
if (args.event !== "PostToolUse")
|
|
27
|
+
return false;
|
|
28
|
+
const tool = usable(args.toolName) ?? usable(args.payload?.tool_name);
|
|
29
|
+
return !!tool && NUDGE_TOOLS.has(tool);
|
|
30
|
+
}
|
|
31
|
+
/** A usable string: non-empty and not an unsubstituted `${…}` hook placeholder. */
|
|
32
|
+
function usable(v) {
|
|
33
|
+
if (typeof v !== "string")
|
|
34
|
+
return null;
|
|
35
|
+
const s = v.trim();
|
|
36
|
+
return s && !s.includes("${") ? s : null;
|
|
37
|
+
}
|
|
38
|
+
/** Sentinel target for a repo-wide search: no path, or the project root. */
|
|
39
|
+
const ROOT = "";
|
|
40
|
+
/**
|
|
41
|
+
* The project-relative POSIX target, {@link ROOT} for the project root, or
|
|
42
|
+
* null when the nudge has nothing to say about it: outside the project, or
|
|
43
|
+
* under a never-indexed directory.
|
|
44
|
+
*/
|
|
45
|
+
function resolveTarget(projectPath, raw) {
|
|
46
|
+
const root = path.resolve(projectPath);
|
|
47
|
+
const rel = path.relative(root, path.resolve(root, raw));
|
|
48
|
+
if (!rel)
|
|
49
|
+
return ROOT;
|
|
50
|
+
if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel))
|
|
51
|
+
return null;
|
|
52
|
+
const segments = rel.split(path.sep);
|
|
53
|
+
if (segments.some((seg) => SKIP_SEGMENTS.has(seg)))
|
|
54
|
+
return null;
|
|
55
|
+
return segments.join("/");
|
|
56
|
+
}
|
|
57
|
+
/** ripgrep `--type` names whose extension differs from the name. */
|
|
58
|
+
const RG_TYPE_EXT = {
|
|
59
|
+
rust: "rs",
|
|
60
|
+
python: "py",
|
|
61
|
+
typescript: "ts",
|
|
62
|
+
javascript: "js",
|
|
63
|
+
ruby: "rb",
|
|
64
|
+
csharp: "cs",
|
|
65
|
+
kotlin: "kt",
|
|
66
|
+
markdown: "md",
|
|
67
|
+
golang: "go",
|
|
68
|
+
};
|
|
69
|
+
/** True when `ext` (with or without the dot) is a Compass-indexed extension. */
|
|
70
|
+
function indexedExt(ext) {
|
|
71
|
+
return langForPath(`x.${ext.replace(/^\./, "")}`) !== undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The extensions a glob restricts matches to, expanding `*.{ts,md}` braces;
|
|
75
|
+
* an empty list when the last segment names no extension (e.g. `**\/*`).
|
|
76
|
+
*/
|
|
77
|
+
function globExtensions(glob) {
|
|
78
|
+
const last = glob.split("/").pop() ?? glob;
|
|
79
|
+
const brace = /\.\{([^}]*)\}$/.exec(last);
|
|
80
|
+
if (brace)
|
|
81
|
+
return brace[1]
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((e) => e.trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
const ext = path.extname(last);
|
|
86
|
+
return ext && !/[*?[]/.test(ext) ? [ext.slice(1)] : [];
|
|
87
|
+
}
|
|
88
|
+
/** A glob is code-targeting when it is extension-less or names an indexed extension. */
|
|
89
|
+
function globTargetsCode(glob) {
|
|
90
|
+
const exts = globExtensions(glob);
|
|
91
|
+
return exts.length === 0 || exts.some(indexedExt);
|
|
92
|
+
}
|
|
93
|
+
/** Grep is restricted to non-indexed files when its `glob` or `type` says so. */
|
|
94
|
+
function grepRestrictedAway(input) {
|
|
95
|
+
const glob = usable(input.glob);
|
|
96
|
+
if (glob && !globTargetsCode(glob))
|
|
97
|
+
return true;
|
|
98
|
+
const type = usable(input.type);
|
|
99
|
+
if (type && !indexedExt(RG_TYPE_EXT[type] ?? type))
|
|
100
|
+
return true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a Read/Grep/Glob of `target` is code access the nudge cares about.
|
|
105
|
+
*
|
|
106
|
+
* - Read: the file has an indexed extension.
|
|
107
|
+
* - Grep: an indexed file, a directory, or a repo-wide search ({@link ROOT}),
|
|
108
|
+
* unless `glob`/`type` restrict it to non-indexed extensions.
|
|
109
|
+
* - Glob: an indexed file, a directory, or a repo-wide search, when `pattern`
|
|
110
|
+
* names an indexed extension or none at all (e.g. `**\/*`); a directory with
|
|
111
|
+
* no pattern stays eligible.
|
|
112
|
+
*/
|
|
113
|
+
function eligible(projectPath, tool, target, input) {
|
|
114
|
+
if (target === ROOT)
|
|
115
|
+
return tool !== "Read" && searchEligible(tool, "", input);
|
|
116
|
+
const dir = isDirectory(projectPath, target);
|
|
117
|
+
if (tool === "Read")
|
|
118
|
+
return dir !== true && indexedExt(path.extname(target) || ".");
|
|
119
|
+
// A directory has no extension, even when its name has a dot (`src/v1.2`).
|
|
120
|
+
const ext = dir === true ? "" : path.extname(target);
|
|
121
|
+
return searchEligible(tool, ext, input, target);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether the target is a directory: one cheap `statSync`, no index access.
|
|
125
|
+
* Undefined when it cannot be stat'ed (missing, permissions) — callers then
|
|
126
|
+
* fall back to the extension heuristic.
|
|
127
|
+
*/
|
|
128
|
+
function isDirectory(projectPath, target) {
|
|
129
|
+
try {
|
|
130
|
+
return fs.statSync(path.join(projectPath, target)).isDirectory();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Grep/Glob eligibility once the target's extension (`""` for a directory) is known. */
|
|
137
|
+
function searchEligible(tool, ext, input, target = ROOT) {
|
|
138
|
+
if (ext && !indexedExt(ext))
|
|
139
|
+
return false;
|
|
140
|
+
if (tool === "Grep")
|
|
141
|
+
return !grepRestrictedAway(input);
|
|
142
|
+
const pattern = usable(input.pattern);
|
|
143
|
+
if (!pattern)
|
|
144
|
+
return target !== ROOT;
|
|
145
|
+
return globTargetsCode(pattern);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Evaluate the Compass-first nudge for one check call.
|
|
149
|
+
*
|
|
150
|
+
* Returns the nudge text — after recording a `nudge` entry in the call log for
|
|
151
|
+
* the rate limit — or null when the event is not an eligible `PostToolUse`
|
|
152
|
+
* Read/Grep/Glob, a Compass evidence call landed in the last 10 minutes, or a
|
|
153
|
+
* nudge already fired in the last 5 minutes. Never throws.
|
|
154
|
+
*
|
|
155
|
+
* @param args - The check call's project, event, tool, and raw payload.
|
|
156
|
+
* @param now - Current epoch ms (injectable for tests).
|
|
157
|
+
*/
|
|
158
|
+
export function compassNudge(args, now = Date.now()) {
|
|
159
|
+
try {
|
|
160
|
+
if (args.event !== "PostToolUse")
|
|
161
|
+
return null;
|
|
162
|
+
const payload = args.payload ?? {};
|
|
163
|
+
const tool = usable(args.toolName) ?? usable(payload.tool_name);
|
|
164
|
+
if (!tool || !NUDGE_TOOLS.has(tool))
|
|
165
|
+
return null;
|
|
166
|
+
const input = (payload.tool_input ?? payload.toolInput ?? payload);
|
|
167
|
+
// No path on Grep/Glob means the cwd: a repo-wide search.
|
|
168
|
+
const raw = usable(input.file_path) ?? usable(input.path) ?? ".";
|
|
169
|
+
const target = resolveTarget(args.projectPath, raw);
|
|
170
|
+
if (target === null || !eligible(args.projectPath, tool, target, input))
|
|
171
|
+
return null;
|
|
172
|
+
const pattern = usable(input.pattern);
|
|
173
|
+
const recent = readCompassCalls(args.projectPath, { sinceMs: now - NUDGE_EVIDENCE_WINDOW_MS });
|
|
174
|
+
if (recent.some((c) => isEvidenceTool(c.tool)))
|
|
175
|
+
return null;
|
|
176
|
+
const rateFloor = now - NUDGE_RATE_LIMIT_MS;
|
|
177
|
+
if (recent.some((c) => c.tool === NUDGE_ENTRY && Date.parse(c.at) >= rateFloor))
|
|
178
|
+
return null;
|
|
179
|
+
recordCompassCall(args.projectPath, NUDGE_ENTRY, new Date(now));
|
|
180
|
+
const stem = target === ROOT ? "" : path.basename(target, path.extname(target));
|
|
181
|
+
const query = tool === "Grep" && pattern ? pattern : stem || pattern || "<concept>";
|
|
182
|
+
const symbol = stem || (tool === "Grep" && pattern ? pattern : "<symbol>");
|
|
183
|
+
return (`Compass first: no compass_find / compass_explore call in the last 10 min. ` +
|
|
184
|
+
`Try \`compass_explore ${symbol}\` or \`compass_find "${query}"\` before reading code.`);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -11,7 +11,13 @@ const SPECLAW_HOOK_INPUT = {
|
|
|
11
11
|
payload: {
|
|
12
12
|
hook_event_name: "${hook_event_name}",
|
|
13
13
|
tool_name: "${tool_name}",
|
|
14
|
-
tool_input: {
|
|
14
|
+
tool_input: {
|
|
15
|
+
file_path: "${tool_input.file_path}",
|
|
16
|
+
path: "${tool_input.path}",
|
|
17
|
+
pattern: "${tool_input.pattern}",
|
|
18
|
+
glob: "${tool_input.glob}",
|
|
19
|
+
type: "${tool_input.type}",
|
|
20
|
+
},
|
|
15
21
|
},
|
|
16
22
|
};
|
|
17
23
|
/** The speclaw hook object — its `{type, server}` pair is the merge identity. */
|
|
@@ -24,6 +30,12 @@ const SPECLAW_HOOK = {
|
|
|
24
30
|
};
|
|
25
31
|
/** Tool-name matcher for the file-mutating tools the `path` backend can evaluate. */
|
|
26
32
|
const MUTATION_MATCHER = "Write|Edit|MultiEdit|NotebookEdit";
|
|
33
|
+
/**
|
|
34
|
+
* Tool-name matcher for the code-reading tools the Compass-first nudge watches.
|
|
35
|
+
* Installed on `PostToolUse` only: there the result is context the agent reads
|
|
36
|
+
* and the event cannot gate the tool; a `PreToolUse` "allow" would auto-approve.
|
|
37
|
+
*/
|
|
38
|
+
export const NUDGE_MATCHER = "Read|Grep|Glob";
|
|
27
39
|
/** True when a hook object is one speclaw owns (safe to replace on merge). */
|
|
28
40
|
function isSpeclawHook(h) {
|
|
29
41
|
const o = h;
|
|
@@ -33,7 +45,8 @@ function isSpeclawHook(h) {
|
|
|
33
45
|
* Compile a law manifest into the hook groups speclaw contributes, one per event
|
|
34
46
|
* the laws demand: `PreToolUse` when any `bloqueo` law exists, `PostToolUse` for
|
|
35
47
|
* `feedback`, `Stop` for `gate`, and `InstructionsLoaded` whenever any law exists
|
|
36
|
-
* (the context-coverage audit).
|
|
48
|
+
* (the context-coverage audit). One `PostToolUse` group matching `Read|Grep|Glob`
|
|
49
|
+
* is always emitted for the Compass-first nudge, even with no laws. A law whose scope contains a malformed glob is
|
|
37
50
|
* excluded and reported, so a bad pattern fails loudly at generation rather than
|
|
38
51
|
* silently matching nothing at runtime.
|
|
39
52
|
*
|
|
@@ -56,8 +69,10 @@ export function compileHooks(manifest) {
|
|
|
56
69
|
const hasGate = valid.some((l) => l.enforcement === "gate");
|
|
57
70
|
if (hasBloqueo)
|
|
58
71
|
byEvent.PreToolUse = [{ matcher: MUTATION_MATCHER, hooks: [{ ...SPECLAW_HOOK }] }];
|
|
72
|
+
// The Compass-first nudge entry is always present, laws or not.
|
|
73
|
+
byEvent.PostToolUse = [{ matcher: NUDGE_MATCHER, hooks: [{ ...SPECLAW_HOOK }] }];
|
|
59
74
|
if (hasFeedback)
|
|
60
|
-
byEvent.PostToolUse
|
|
75
|
+
byEvent.PostToolUse.unshift({ matcher: MUTATION_MATCHER, hooks: [{ ...SPECLAW_HOOK }] });
|
|
61
76
|
if (hasGate)
|
|
62
77
|
byEvent.Stop = [{ hooks: [{ ...SPECLAW_HOOK }] }];
|
|
63
78
|
if (valid.length > 0)
|
|
@@ -9,7 +9,9 @@ import { digestText, discoverIntegrityPaths, integrityPolicy, isRegenerableIdeMi
|
|
|
9
9
|
import { loadScanSuppressions, scanPaths } from "./scan.js";
|
|
10
10
|
/**
|
|
11
11
|
* Verify digests and/or scan rule files. Missing lockfile is soft (ok=true)
|
|
12
|
-
* with guidance to run `speclaw laws lock`.
|
|
12
|
+
* with guidance to run `speclaw laws lock`. Lock entries for regenerable IDE
|
|
13
|
+
* mirrors (files or the `.claude/rules/speclaw` symlink) that are missing or
|
|
14
|
+
* retargeted only warn; other managed symlinks fail strictly.
|
|
13
15
|
*/
|
|
14
16
|
export function verifyIntegrity(opts) {
|
|
15
17
|
const projectPath = opts.projectPath;
|
|
@@ -174,6 +176,11 @@ export function verifyIntegrity(opts) {
|
|
|
174
176
|
catch {
|
|
175
177
|
actual = null;
|
|
176
178
|
}
|
|
179
|
+
// Entries for regenerable mirror links come from locks written before
|
|
180
|
+
// they stopped being pinned; the link targets gitignored `ai-specs/` and
|
|
181
|
+
// is absent or dangling on clean clones, so it warns only and never
|
|
182
|
+
// fails CI.
|
|
183
|
+
const regenerable = isRegenerableIdeMirror(rel);
|
|
177
184
|
if (actual === null) {
|
|
178
185
|
symlinks.push({
|
|
179
186
|
path: rel,
|
|
@@ -181,13 +188,16 @@ export function verifyIntegrity(opts) {
|
|
|
181
188
|
actualTarget: null,
|
|
182
189
|
status: "missing",
|
|
183
190
|
});
|
|
184
|
-
|
|
191
|
+
if (!regenerable)
|
|
192
|
+
ok = false;
|
|
185
193
|
verifyFindings.push({
|
|
186
194
|
lawId: "integrity~symlink~1",
|
|
187
|
-
severity: "error",
|
|
195
|
+
severity: regenerable ? "warn" : "error",
|
|
188
196
|
engine: "integrity",
|
|
189
197
|
file: rel,
|
|
190
|
-
message:
|
|
198
|
+
message: regenerable
|
|
199
|
+
? "Regenerable IDE mirror symlink missing — run `speclaw update` or `speclaw laws lock` to drop the stale entry"
|
|
200
|
+
: `Managed symlink missing (expected → ${entry.target})`,
|
|
191
201
|
});
|
|
192
202
|
}
|
|
193
203
|
else if (normalizeLink(actual) !== normalizeLink(entry.target)) {
|
|
@@ -197,13 +207,16 @@ export function verifyIntegrity(opts) {
|
|
|
197
207
|
actualTarget: actual,
|
|
198
208
|
status: "mismatch",
|
|
199
209
|
});
|
|
200
|
-
|
|
210
|
+
if (!regenerable)
|
|
211
|
+
ok = false;
|
|
201
212
|
verifyFindings.push({
|
|
202
213
|
lawId: "integrity~symlink~1",
|
|
203
|
-
severity: "error",
|
|
214
|
+
severity: regenerable ? "warn" : "error",
|
|
204
215
|
engine: "integrity",
|
|
205
216
|
file: rel,
|
|
206
|
-
message:
|
|
217
|
+
message: regenerable
|
|
218
|
+
? "Regenerable IDE mirror symlink retargeted — run `speclaw update` or `speclaw laws lock` to drop the stale entry"
|
|
219
|
+
: `Managed symlink retargeted`,
|
|
207
220
|
detail: `expected ${entry.target} found ${actual}`,
|
|
208
221
|
});
|
|
209
222
|
}
|
|
@@ -106,13 +106,13 @@ export function buildLock(opts) {
|
|
|
106
106
|
/** Integrity severity policy for a project-relative path. */
|
|
107
107
|
export function integrityPolicy(relPath) {
|
|
108
108
|
const n = relPath.split("\\").join("/");
|
|
109
|
-
//
|
|
110
|
-
//
|
|
109
|
+
// Regenerable IDE mirrors of gitignored `ai-specs/` (`.cursor/rules/`, the
|
|
110
|
+
// `.claude/rules/speclaw` link) fall through to scan-only: lock/CI must not
|
|
111
|
+
// treat them as strict committed files; scan when present, never pin.
|
|
111
112
|
if (n === "AGENTS.md" ||
|
|
112
113
|
n === "CLAUDE.md" ||
|
|
113
114
|
n.startsWith(".github/instructions/") ||
|
|
114
|
-
n === ".coderabbit.yaml"
|
|
115
|
-
n === ".claude/rules/speclaw") {
|
|
115
|
+
n === ".coderabbit.yaml") {
|
|
116
116
|
return "strict";
|
|
117
117
|
}
|
|
118
118
|
if (n === "LAWS.md" || n === "docs/compass.md" || n.startsWith("docs/standards/")) {
|
|
@@ -120,10 +120,22 @@ export function integrityPolicy(relPath) {
|
|
|
120
120
|
}
|
|
121
121
|
return "scan-only";
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* True when a path is an IDE mirror of regenerable (typically gitignored) content.
|
|
125
|
+
*
|
|
126
|
+
* Covers the `.claude/rules/speclaw` symlink as well as mirror files: the link
|
|
127
|
+
* targets gitignored `ai-specs/rules`, so on a clean clone it is absent (when
|
|
128
|
+
* the project ignores it) or dangling, and must never be pinned in, or fail
|
|
129
|
+
* verify against, the lock.
|
|
130
|
+
*
|
|
131
|
+
* @param relPath - Project-relative path (file or symlink).
|
|
132
|
+
* @returns Whether lock/verify should treat the path as regenerable.
|
|
133
|
+
*/
|
|
124
134
|
export function isRegenerableIdeMirror(relPath) {
|
|
125
135
|
const n = relPath.split("\\").join("/");
|
|
126
|
-
return (n
|
|
136
|
+
return (n === ".claude/rules/speclaw" ||
|
|
137
|
+
n.startsWith(".claude/rules/speclaw/") ||
|
|
138
|
+
n.startsWith(".cursor/rules/") ||
|
|
127
139
|
n.startsWith(".cursor/skills/") ||
|
|
128
140
|
n.startsWith(".cursor/commands/") ||
|
|
129
141
|
n.startsWith(".claude/skills/") ||
|
|
@@ -226,6 +238,8 @@ export function prepareIntegrityText(relPath, raw) {
|
|
|
226
238
|
* Snapshot digests for discovered files with ownership policy.
|
|
227
239
|
* For `.coderabbit.yaml`, digests only the speclaw delimited block when present.
|
|
228
240
|
* For `docs/compass.md`, digests with the regenerable map body stripped.
|
|
241
|
+
* Regenerable IDE mirror symlinks ({@link isRegenerableIdeMirror}) are never
|
|
242
|
+
* pinned, so the lock is identical with or without the local link.
|
|
229
243
|
*/
|
|
230
244
|
export function snapshotLockEntries(projectPath) {
|
|
231
245
|
const { files: paths, symlinks } = discoverIntegrityPaths(projectPath);
|
|
@@ -239,8 +253,11 @@ export function snapshotLockEntries(projectPath) {
|
|
|
239
253
|
files[rel] = { digest: digestText(raw), ownership };
|
|
240
254
|
}
|
|
241
255
|
const symlinkMap = {};
|
|
242
|
-
for (const s of symlinks)
|
|
256
|
+
for (const s of symlinks) {
|
|
257
|
+
if (isRegenerableIdeMirror(s.path))
|
|
258
|
+
continue;
|
|
243
259
|
symlinkMap[s.path] = { target: s.target };
|
|
260
|
+
}
|
|
244
261
|
return { files, symlinks: symlinkMap };
|
|
245
262
|
}
|
|
246
263
|
/** Extract a speclaw-marked region from coderabbit yaml if present. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: archiver
|
|
3
3
|
description: Reconcile specs, sync, and archive a change when Cortex stage is archiving. No new feature implementation.
|
|
4
|
-
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__lawbook_change,
|
|
4
|
+
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__lawbook_change, mcp__speclaw__cortex
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are the **archiver** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: explorer
|
|
3
3
|
description: Read-only codebase investigation for a lawbook change. Use when the Cortex stage is exploring (or investigate for bugs). Compass-first; writes nothing under lawbook/ or src/.
|
|
4
|
-
tools: Read, Grep, Glob, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find,
|
|
4
|
+
tools: Read, Grep, Glob, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__compass_diff_context, mcp__speclaw__compass_index, mcp__speclaw__lawbook_change, mcp__speclaw__lawbook_investigate
|
|
5
5
|
disallowedTools: Write, Edit, MultiEdit, Bash, NotebookEdit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -9,9 +9,12 @@ You are the **explorer** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
|
9
9
|
|
|
10
10
|
## Goal
|
|
11
11
|
|
|
12
|
-
Investigate the idea or bug with Compass **before** any grep/read
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
Investigate the idea or bug with Compass **before** any grep/read: locate with
|
|
13
|
+
`compass_find`, read with `compass_explore`, and run `compass_index` only when
|
|
14
|
+
find returns nothing or reports a missing or stale index. Read/Grep code only
|
|
15
|
+
after naming which Rule 1 fallback holds. Produce a short brief for the
|
|
16
|
+
planner: relevant symbols, blast radius, risks, open questions, and
|
|
17
|
+
`Compass calls made: N`. Do **not** draft change artifacts or edit source.
|
|
15
18
|
|
|
16
19
|
## Skill
|
|
17
20
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: implementer
|
|
3
3
|
description: Implement drafted lawbook tasks in source and tests. Use when Cortex stage is implementing. Does not run final quality gates, review, sync, or archive.
|
|
4
|
-
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find,
|
|
4
|
+
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__compass_diff_context, mcp__speclaw__compass_index, mcp__speclaw__lawbook_change, mcp__speclaw__cortex
|
|
5
5
|
disallowedTools: NotebookEdit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: planner
|
|
3
3
|
description: Draft lawbook change artifacts and ceremony level. Use when Cortex stage is planning. Asks clarifying questions via the coordinator; never edits src/.
|
|
4
|
-
tools: Read, Grep, Glob, Write, Edit, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__lawbook_change
|
|
4
|
+
tools: Read, Grep, Glob, Write, Edit, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__lawbook_change
|
|
5
5
|
disallowedTools: Bash, NotebookEdit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -11,7 +11,8 @@ You are the **planner** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
|
11
11
|
|
|
12
12
|
Turn the explorer brief + user intent into a reviewable change under
|
|
13
13
|
`lawbook/changes/<name>/` at the confirmed ceremony level. Prefer `draft` /
|
|
14
|
-
`quick` skills
|
|
14
|
+
`quick` skills; scaffold with `lawbook_change` action `draft` (`change`,
|
|
15
|
+
`level`).
|
|
15
16
|
|
|
16
17
|
A complete explorer brief is the code map. Do not re-investigate it. Call
|
|
17
18
|
Compass only for a gap the brief names.
|
|
@@ -19,8 +20,10 @@ Compass only for a gap the brief names.
|
|
|
19
20
|
## Questions
|
|
20
21
|
|
|
21
22
|
If anything material is ambiguous, **stop**. Return numbered questions to the
|
|
22
|
-
coordinator (do not invent answers).
|
|
23
|
-
|
|
23
|
+
coordinator (do not invent answers). When dispatched for questions only, return
|
|
24
|
+
the questions and draft nothing. The coordinator batches them with the explorer
|
|
25
|
+
open questions and the level confirmation into one `pauseForQuestions` round.
|
|
26
|
+
After the human answers, draft at the level the coordinator recorded.
|
|
24
27
|
|
|
25
28
|
## Hard constraints
|
|
26
29
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Deep review of implemented change without patching code. Writes reports/review.md with PASS or FAIL. Use when Cortex stage is reviewing.
|
|
4
|
-
tools: Read, Grep, Glob, Write, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find,
|
|
4
|
+
tools: Read, Grep, Glob, Write, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_find, mcp__speclaw__compass_diff_context, mcp__speclaw__lawbook_change, mcp__speclaw__cortex
|
|
5
5
|
disallowedTools: Edit, MultiEdit, Bash, NotebookEdit
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tester
|
|
3
3
|
description: Run quality gates and manual verification; write discipline reports. Use when Cortex stage is testing. May add missing tests only — not features.
|
|
4
|
-
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore,
|
|
4
|
+
tools: Read, Grep, Glob, Write, Edit, Bash, CallMcpTool, mcp__speclaw__compass_explore, mcp__speclaw__compass_diff_context, mcp__speclaw__lawbook_change, mcp__speclaw__cortex
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are the **tester** role in speclaw's **Cortex** (One brain. Many agents.).
|
|
@@ -4,8 +4,8 @@ description: Draft a new spec-driven change at the confirmed ceremony level befo
|
|
|
4
4
|
|
|
5
5
|
Draft a new change under `lawbook/changes/<name>/` for: $ARGUMENTS
|
|
6
6
|
|
|
7
|
-
Follow the `draft` skill: ensure `lawbook/` exists (`
|
|
8
|
-
with Compass, propose a ceremony level (`
|
|
7
|
+
Follow the `draft` skill: ensure `lawbook/` exists (`lawbook_change` action `init`), investigate
|
|
8
|
+
with Compass, propose a ceremony level (`lawbook_change` action `level`, mode `propose`) and
|
|
9
9
|
**confirm** it with the human (`set`), then scaffold only the artifacts that
|
|
10
10
|
level requires. For true one-liners use `speclaw quick` / the `quick` skill
|
|
11
11
|
instead. Finish by running `lawbook_change` (action: validate) and fixing every issue.
|
|
@@ -6,9 +6,9 @@ Scaffold a **ceremony level 0** change named `$ARGUMENTS` with `speclaw quick`
|
|
|
6
6
|
(or the equivalent `scaffoldQuick` path). Do **not** invent `proposal.md` /
|
|
7
7
|
`design.md` / delta specs for a true one-liner.
|
|
8
8
|
|
|
9
|
-
1. Ensure `lawbook/` exists (`
|
|
9
|
+
1. Ensure `lawbook/` exists (`lawbook_change` action `init` if needed).
|
|
10
10
|
2. Prefer passing `--path` / `--symbol` so the proposal rationale is measured.
|
|
11
11
|
3. Confirm with the human if the measured proposal is higher than 0 — promote
|
|
12
|
-
(`
|
|
12
|
+
(`lawbook_change` action `level`, mode `promote`) instead of staying at quick.
|
|
13
13
|
4. Implement, check the checklist in `record.md`, write a discipline report
|
|
14
14
|
under `reports/`, then archive (no sync required at level 0).
|
|
@@ -70,7 +70,7 @@ in.
|
|
|
70
70
|
|
|
71
71
|
## 4. Reports gate the archive
|
|
72
72
|
|
|
73
|
-
`
|
|
73
|
+
`lawbook_change` action `archive` refuses to archive while `reports/` holds no discipline report
|
|
74
74
|
(the `reports/README.md` scaffold does not count). Which disciplines a change
|
|
75
75
|
touched — and therefore which reports are owed, including `api.md` for any
|
|
76
76
|
API-touching change — is the agent's responsibility to judge and satisfy before
|
|
@@ -27,8 +27,8 @@ steps, branch convention, and testing/documentation requirements.
|
|
|
27
27
|
see the `spec-reports-disciplines` rule) with the unit/integration/e2e results
|
|
28
28
|
for what the feature touched.
|
|
29
29
|
- Update the technical documentation the change touches.
|
|
30
|
-
- Archive the change within the same PR (the `archive` command / `
|
|
31
|
-
|
|
30
|
+
- Archive the change within the same PR (the `archive` command / `lawbook_change`
|
|
31
|
+
action `archive`) after Cortex review/test PASS.
|
|
32
32
|
|
|
33
33
|
## 3. Manual verification — the tester must execute it
|
|
34
34
|
|
|
@@ -38,10 +38,10 @@ requires verification is not complete until the tester has verified it.
|
|
|
38
38
|
|
|
39
39
|
## 4. Archiving is part of the change
|
|
40
40
|
|
|
41
|
-
A change is not done until it is archived with `
|
|
41
|
+
A change is not done until it is archived with `lawbook_change` action `archive` (never a manual
|
|
42
42
|
`mv`). The archive lands in the same PR that implements the change.
|
|
43
43
|
|
|
44
|
-
`
|
|
44
|
+
Archive (`lawbook_change` action `archive`) is gated: it refuses to archive while any task is unchecked,
|
|
45
45
|
while `reports/` has no discipline report, while the delta specs are not yet
|
|
46
46
|
synced into the canonical specs, or while harness review/test verdicts are not
|
|
47
47
|
PASS. Resolve those first — the gate is enforced in the engine, so a manual `mv`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Reconciliation review (agent-executed)
|
|
2
2
|
|
|
3
3
|
Run the reconciliation from the `sync` skill: reconstruct what was built
|
|
4
|
-
(branch diff since draft + `compass_explore`
|
|
4
|
+
(branch diff since draft + `compass_explore` with include `blast_radius`) and compare it
|
|
5
5
|
to the change's delta specs.
|
|
6
6
|
|
|
7
7
|
- **If the code drifted past the contracts:** show short insights — a tight
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Load the change
|
|
2
2
|
|
|
3
3
|
- Read `lawbook/changes/<name>/proposal.md`, `tasks.md`, and the delta specs under
|
|
4
|
-
`specs/`. If unsure which change, run `
|
|
4
|
+
`specs/`. If unsure which change, run `lawbook_change` (action: list).
|
|
5
5
|
- Read the governing standards in `docs/standards/` for the areas you'll touch.
|
|
6
6
|
|
|
7
7
|
Next: read `steps/02-branch.md` and do only what it says.
|
|
@@ -3,10 +3,26 @@
|
|
|
3
3
|
For the current Cortex stage (`cortex` action `brief` or `status`):
|
|
4
4
|
|
|
5
5
|
1. Dispatch (spawn or adopt) the matching role with a clear handoff brief.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
- **Explorer template.** Give the intent (what the change should achieve),
|
|
7
|
+
the symbols or concepts to locate, and the questions to answer. Do not
|
|
8
|
+
list file paths for the explorer to read — it locates code through
|
|
9
|
+
Compass (`compass_find` / `compass_explore`) and reports
|
|
10
|
+
`Compass calls made: N`.
|
|
11
|
+
- When the role is the planner, paste the explorer brief into the prompt
|
|
12
|
+
unchanged: symbols with files, callers and callees, blast radius,
|
|
13
|
+
standards already read, recommended approach, open questions, and gaps.
|
|
14
|
+
2. **Single question round** (after the explorer, before the planner drafts):
|
|
15
|
+
1. Propose a level with `lawbook_change` action `level`, mode `propose`,
|
|
16
|
+
passing the paths and symbols from the brief.
|
|
17
|
+
2. Dispatch the planner for questions only (no artifacts yet).
|
|
18
|
+
3. Merge the explorer open questions, the planner questions, and the level
|
|
19
|
+
confirmation into **one** `advance` with `pauseForQuestions` +
|
|
20
|
+
`openQuestions`. Ask the human once; never open a second round for the
|
|
21
|
+
level.
|
|
22
|
+
4. After the answers, record the level with `lawbook_change` action
|
|
23
|
+
`level`, mode `set` (evidence and answers in `reason`), then redispatch
|
|
24
|
+
the planner to draft.
|
|
25
|
+
3. On role completion, call the matching Cortex op:
|
|
10
26
|
- exploring → `advance`
|
|
11
27
|
- planning with questions → `advance` + `pauseForQuestions` + `openQuestions`
|
|
12
28
|
- questions after human answers → `advance` (back to planning)
|
|
@@ -15,8 +31,8 @@ For the current Cortex stage (`cortex` action `brief` or `status`):
|
|
|
15
31
|
- reviewing PASS → `advance` + `verdict: PASS`; FAIL → `rework` + `verdict: FAIL`
|
|
16
32
|
- testing PASS → `advance` + `verdict: PASS`; FAIL → `rework` + `verdict: FAIL`
|
|
17
33
|
- archiving success → `advance` (to done)
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
4. Max rework is 3; if the engine rejects further rework, **ask the human**.
|
|
35
|
+
5. Coordinator never edits `src/`, never archives, never pushes, never runs
|
|
20
36
|
`laws accept`.
|
|
21
37
|
|
|
22
38
|
Next: read `steps/03-complete.md` and do only what it says.
|
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
concept) before grep/read to locate the code and its blast radius. Read the
|
|
11
11
|
governing standards in `docs/standards/`.
|
|
12
12
|
- Clarify what the user wants (feature / fix / refactor) and confirm scope.
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
- When `change.json` already has a `confirmedLevel` (the coordinator recorded
|
|
14
|
+
it in the single question round), use that level and skip the separate level
|
|
15
|
+
confirmation.
|
|
16
|
+
- Otherwise **propose a ceremony level** with `lawbook_change` action `level`
|
|
17
|
+
(mode `propose`) using the paths and symbols from the brief or from that
|
|
18
|
+
locate pass; **confirm with the human** (mode `set`) before writing
|
|
19
|
+
artifacts. For an obvious one-liner, offer `speclaw quick` instead.
|
|
17
20
|
|
|
18
21
|
Next: read `steps/03-name-capabilities.md` and do only what it says.
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
- **Change name:** kebab-case, action-oriented (e.g. `add-login`,
|
|
4
4
|
`fix-shift-overlap`). This is the folder under `lawbook/changes/`, and it is
|
|
5
5
|
per-feature — always distinct.
|
|
6
|
-
- **Capabilities:** run `
|
|
6
|
+
- **Capabilities:** run `lawbook_change` (action: list) to see the canonical capabilities. A
|
|
7
7
|
capability is the living contract for an area of behavior — it is *not* the
|
|
8
8
|
change. When your change modifies behavior an existing capability already
|
|
9
9
|
governs, reuse that capability's **exact** name so `sync` updates its spec.
|
|
10
10
|
Introduce a new capability only as a deliberate choice for a genuinely distinct
|
|
11
11
|
area of behavior — never as a near-duplicate (`transfer` next to an existing
|
|
12
12
|
`transfers`) of one that already exists.
|
|
13
|
+
- Pass that exact capability name to the scaffold (`--capability` /
|
|
14
|
+
`capability`), so the delta stub lands in the right capability.
|
|
13
15
|
|
|
14
16
|
Next: read `steps/04-write-artifacts.md` and do only what it says.
|