@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,197 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
// Covers: req~compass-call-log~1
|
|
4
|
+
/** Live call-log file name under `.speclaw/`. */
|
|
5
|
+
export const COMPASS_CALL_LOG = "compass-calls.jsonl";
|
|
6
|
+
/** The live log rotates to `.1` once it grows past this many bytes. */
|
|
7
|
+
export const CALL_LOG_ROTATE_BYTES = 256 * 1024;
|
|
8
|
+
/** Readers never look further back than this many bytes of the live log. */
|
|
9
|
+
export const CALL_LOG_READ_BYTES = 64 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* Compass tools whose calls count as "looked through the graph" evidence.
|
|
12
|
+
* `compass_index` and nudge entries are logged too but are not evidence.
|
|
13
|
+
*/
|
|
14
|
+
export const EVIDENCE_TOOLS = new Set([
|
|
15
|
+
"compass_explore",
|
|
16
|
+
"compass_find",
|
|
17
|
+
"compass_diff_context",
|
|
18
|
+
"compass_impact",
|
|
19
|
+
"compass_trace",
|
|
20
|
+
"compass_search",
|
|
21
|
+
"compass_recall",
|
|
22
|
+
]);
|
|
23
|
+
/** Tool name the Compass-first nudge records for its rate limit. */
|
|
24
|
+
export const NUDGE_ENTRY = "nudge";
|
|
25
|
+
/** True when `tool` counts as Compass evidence. */
|
|
26
|
+
export function isEvidenceTool(tool) {
|
|
27
|
+
return EVIDENCE_TOOLS.has(tool);
|
|
28
|
+
}
|
|
29
|
+
function logPath(projectPath) {
|
|
30
|
+
return path.join(projectPath, ".speclaw", COMPASS_CALL_LOG);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Rotate the live log to `.1` when it is over the cap, tolerating a race with
|
|
34
|
+
* other processes doing the same.
|
|
35
|
+
*
|
|
36
|
+
* The live file is first renamed to a name unique to this process, so two
|
|
37
|
+
* rotators can never both move the same file onto `.1`. If what was claimed is
|
|
38
|
+
* not over the cap, another process rotated first and this one grabbed its
|
|
39
|
+
* fresh live file: those lines are appended back to the live log instead of
|
|
40
|
+
* replacing `.1`, so `.1` always holds a full previous generation and no
|
|
41
|
+
* recent entry disappears. A missing live file (lost race) is ignored.
|
|
42
|
+
*
|
|
43
|
+
* @param file - Absolute path of the live log.
|
|
44
|
+
*/
|
|
45
|
+
export function rotateCallLog(file) {
|
|
46
|
+
const claimed = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
47
|
+
try {
|
|
48
|
+
fs.renameSync(file, claimed);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return; // nothing to rotate, or another process already took it
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
if (fs.statSync(claimed).size > CALL_LOG_ROTATE_BYTES) {
|
|
55
|
+
fs.renameSync(claimed, `${file}.1`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
fs.appendFileSync(file, fs.readFileSync(claimed));
|
|
59
|
+
fs.rmSync(claimed, { force: true });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* best-effort: a leftover .tmp is harmless and never read */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Append one `{at, tool}` line to `.speclaw/compass-calls.jsonl`.
|
|
67
|
+
*
|
|
68
|
+
* Best effort: creates `.speclaw/` when missing, rotates the live log to
|
|
69
|
+
* `.jsonl.1` (replacing any earlier generation) when it exceeds 256 KiB — see
|
|
70
|
+
* {@link rotateCallLog} — and never throws: a failed write must not fail or
|
|
71
|
+
* delay the Compass tool.
|
|
72
|
+
*
|
|
73
|
+
* @param projectPath - Project root.
|
|
74
|
+
* @param tool - MCP tool name the call maps to.
|
|
75
|
+
* @param at - Call time; defaults to now.
|
|
76
|
+
*/
|
|
77
|
+
export function recordCompassCall(projectPath, tool, at = new Date()) {
|
|
78
|
+
try {
|
|
79
|
+
const file = logPath(projectPath);
|
|
80
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
81
|
+
let size = 0;
|
|
82
|
+
try {
|
|
83
|
+
size = fs.statSync(file).size;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* no live log yet */
|
|
87
|
+
}
|
|
88
|
+
if (size > CALL_LOG_ROTATE_BYTES)
|
|
89
|
+
rotateCallLog(file);
|
|
90
|
+
fs.appendFileSync(file, JSON.stringify({ at: at.toISOString(), tool }) + "\n", "utf8");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* best-effort */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Read the last `maxBytes` of `file`; `fromStart` when the whole file was read. */
|
|
97
|
+
function readTail(file, maxBytes) {
|
|
98
|
+
let fd;
|
|
99
|
+
try {
|
|
100
|
+
fd = fs.openSync(file, "r");
|
|
101
|
+
const size = fs.fstatSync(fd).size;
|
|
102
|
+
// Read one byte before the window so a cut that lands exactly on a line
|
|
103
|
+
// boundary keeps that whole first line.
|
|
104
|
+
const start = Math.max(0, size - maxBytes - 1);
|
|
105
|
+
const len = size - start;
|
|
106
|
+
const buf = Buffer.alloc(len);
|
|
107
|
+
fs.readSync(fd, buf, 0, len, start);
|
|
108
|
+
let raw = buf.toString("utf8");
|
|
109
|
+
// Mid-file: everything up to the first newline is a partial (or the extra
|
|
110
|
+
// boundary byte) and is dropped.
|
|
111
|
+
if (start > 0) {
|
|
112
|
+
const nl = raw.indexOf("\n");
|
|
113
|
+
raw = nl === -1 ? "" : raw.slice(nl + 1);
|
|
114
|
+
}
|
|
115
|
+
return { raw, fromStart: start === 0 };
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
if (fd !== undefined) {
|
|
122
|
+
try {
|
|
123
|
+
fs.closeSync(fd);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* ignore */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Parse log lines, skipping malformed ones. */
|
|
132
|
+
function parseLines(raw) {
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const line of raw.split("\n")) {
|
|
135
|
+
if (!line.trim())
|
|
136
|
+
continue;
|
|
137
|
+
let row;
|
|
138
|
+
try {
|
|
139
|
+
row = JSON.parse(line);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!row || typeof row !== "object")
|
|
145
|
+
continue;
|
|
146
|
+
const { at, tool } = row;
|
|
147
|
+
if (typeof at !== "string" || typeof tool !== "string")
|
|
148
|
+
continue;
|
|
149
|
+
const ms = Date.parse(at);
|
|
150
|
+
if (Number.isNaN(ms))
|
|
151
|
+
continue;
|
|
152
|
+
out.push({ at, tool, ms });
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Read the bounded tail of the call log.
|
|
158
|
+
*
|
|
159
|
+
* Reads at most `maxBytes` from the end of the live file, drops a partial
|
|
160
|
+
* first line, and skips lines that fail to parse. When the whole live file fit
|
|
161
|
+
* in that window but does not reach back to `sinceMs` (or no `sinceMs` was
|
|
162
|
+
* given), the tail of the previous generation `.1` is read too — at most
|
|
163
|
+
* another `maxBytes` — so a rotation never hides evidence from the current
|
|
164
|
+
* window. Returns `[]` when no log exists.
|
|
165
|
+
*
|
|
166
|
+
* @param projectPath - Project root.
|
|
167
|
+
* @param opts - Optional `sinceMs` filter and per-generation tail size.
|
|
168
|
+
*/
|
|
169
|
+
export function readCompassCalls(projectPath, opts = {}) {
|
|
170
|
+
const maxBytes = Math.max(1, opts.maxBytes ?? CALL_LOG_READ_BYTES);
|
|
171
|
+
const file = logPath(projectPath);
|
|
172
|
+
const live = readTail(file, maxBytes);
|
|
173
|
+
const liveRows = live ? parseLines(live.raw) : [];
|
|
174
|
+
let rows = liveRows;
|
|
175
|
+
const oldest = liveRows.length ? Math.min(...liveRows.map((r) => r.ms)) : Infinity;
|
|
176
|
+
const reachesSince = opts.sinceMs !== undefined && oldest <= opts.sinceMs;
|
|
177
|
+
if ((!live || live.fromStart) && !reachesSince) {
|
|
178
|
+
const prev = readTail(`${file}.1`, maxBytes);
|
|
179
|
+
if (prev)
|
|
180
|
+
rows = [...parseLines(prev.raw), ...liveRows];
|
|
181
|
+
}
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const { at, tool, ms } of rows) {
|
|
184
|
+
if (opts.sinceMs !== undefined && ms < opts.sinceMs)
|
|
185
|
+
continue;
|
|
186
|
+
out.push({ at, tool });
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/** Count evidence calls in `calls`. */
|
|
191
|
+
export function countEvidence(calls) {
|
|
192
|
+
let n = 0;
|
|
193
|
+
for (const c of calls)
|
|
194
|
+
if (isEvidenceTool(c.tool))
|
|
195
|
+
n++;
|
|
196
|
+
return n;
|
|
197
|
+
}
|