@esneiderbravo/speclaw 2.0.11 → 2.0.13
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 +5 -1
- package/dist/cli/commands/cortex.js +3 -2
- package/dist/cli/commands/lawbook.js +7 -3
- package/dist/cli/commands/laws.js +10 -2
- package/dist/cli/commands/query.js +2 -2
- package/dist/cli/commands/ship-on-stop.js +46 -0
- package/dist/cli/commands/ship.js +44 -0
- package/dist/cli/commands/update.js +25 -0
- package/dist/cli/index.js +8 -3
- package/dist/cli/lib/args.js +35 -5
- package/dist/cli/lib/help.js +36 -4
- package/dist/modules/compass/db.js +47 -9
- package/dist/modules/compass/explore-rich.js +50 -9
- package/dist/modules/compass/extract.js +220 -0
- package/dist/modules/compass/find-output.js +172 -0
- package/dist/modules/compass/hybrid.js +149 -21
- package/dist/modules/compass/indexer.js +26 -2
- package/dist/modules/compass/query.js +48 -12
- package/dist/modules/compass/register.js +13 -7
- package/dist/modules/cortex/harness.js +17 -4
- package/dist/modules/cortex/status.js +1 -1
- package/dist/modules/foundation/assets/AGENTS.template.md +10 -9
- package/dist/modules/foundation/assets/CLAUDE.template.md +13 -10
- package/dist/modules/foundation/assets/docs/compass.template.md +11 -4
- package/dist/modules/foundation/deps.js +5 -3
- package/dist/modules/foundation/doctor.js +1 -1
- package/dist/modules/foundation/graph.js +5 -5
- package/dist/modules/foundation/hooks.js +30 -4
- package/dist/modules/foundation/integrity.js +11 -3
- package/dist/modules/foundation/laws.js +5 -0
- package/dist/modules/lawbook/assets/commands/cortex.md +1 -1
- package/dist/modules/lawbook/assets/rules/spec-reports-disciplines.md +6 -0
- package/dist/modules/lawbook/assets/rules/spec-tasks-mandatory-steps.md +6 -0
- package/dist/modules/lawbook/assets/skills/cortex/SKILL.md +17 -5
- package/dist/modules/lawbook/bugfix.js +12 -4
- package/dist/modules/lawbook/change-tool.js +7 -0
- package/dist/modules/lawbook/engine.js +3 -0
- package/dist/modules/lawbook/levels.js +35 -4
- package/dist/modules/lawbook/quick.js +17 -6
- package/dist/modules/lawbook/ship.js +325 -0
- package/dist/shared/mcp.js +6 -4
- package/dist/shared/output-budget.js +36 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ gh attestation verify <tarball> --owner esneiderbravo
|
|
|
116
116
|
| Module | What it does |
|
|
117
117
|
| :-- | :-- |
|
|
118
118
|
| **Foundation** | The project's constitution: `LAWS.md` binding granular standards under `docs/standards/`, plus `CLAUDE.md` / `AGENTS.md`. **Enforced** via agent hooks (`speclaw check`), deterministic graph laws (`speclaw verify` / `laws verify`), multidialect compile (`laws compile`), and committed **`speclaw.lock`** digests + injection scan (`laws lock` / `accept` / `scan`). |
|
|
119
|
-
| **Compass** | Local code graph (tree-sitter → `node:sqlite`): hybrid find (FTS5 + vectors + RRF + PageRank), impact, affected-tests, hotspots, coupling, visualize. Schema **
|
|
119
|
+
| **Compass** | Local code graph (tree-sitter → `node:sqlite`): hybrid find (FTS5 + vectors + RRF + PageRank), impact, affected-tests, hotspots, coupling, visualize. Schema **12** (type-reference callers). Refreshed silently at every Claude Code session start once built. No LLM — lives in `.speclaw/` (gitignored). |
|
|
120
120
|
| **Lawbook** | Spec-driven workflow with **adaptive ceremony** (levels 0–3), EARS linting, requirement **coverage**, sealed **drift** anchors, bugfix + `lawbook_investigate`. Artifacts under `lawbook/`. |
|
|
121
121
|
| **Cortex** | **One brain. Many agents.** The multi-agent loop: durable `harness.json`, MCP tool `cortex` (`status`/`start`/`advance`/`rework`/`brief`), CLI `speclaw cortex`, skill `/lawbook/cortex`. Dispatches explorer → planner → implementer → reviewer → tester → archiver. |
|
|
122
122
|
| **Team** | Declare `team.owners` in `lawbook/config.yaml`; `speclaw owners --write` compiles a managed trailing block in `.github/CODEOWNERS` (GitHub: last match wins). Doctor checks the posture. CLI-only — no MCP tool. |
|
|
@@ -310,6 +310,10 @@ speclaw laws accept AGENTS.md # interactive TTY only — never via MCP
|
|
|
310
310
|
speclaw laws lock --force # list drifted files, confirm, re-baseline — interactive TTY only
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
+
`speclaw laws scan` (text or `--json`) exits 1 when `speclaw.lock` cannot be
|
|
314
|
+
read or a finding has severity `error`. On an unreadable lock it names the lock
|
|
315
|
+
error (`lockError` in the JSON) and still reports every injection finding.
|
|
316
|
+
|
|
313
317
|
A lock refresh (`init`, `update`, `laws compile`, `laws lock`) never launders an
|
|
314
318
|
edit: a strict file (`CLAUDE.md`, `AGENTS.md`, compiled rules) that drifted from
|
|
315
319
|
the lock outside speclaw **keeps its locked digest**, and the command warns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { handleHarness, readHarness } from "../../modules/cortex/harness.js";
|
|
2
2
|
import { briefForStage } from "../../modules/cortex/brief.js";
|
|
3
|
-
import {
|
|
3
|
+
import { repeated } from "../lib/args.js";
|
|
4
4
|
import { ui } from "../lib/ui.js";
|
|
5
5
|
const OPS = ["status", "start", "advance", "rework", "brief"];
|
|
6
6
|
/**
|
|
@@ -36,7 +36,8 @@ export async function runCortex(flags) {
|
|
|
36
36
|
change: changeName,
|
|
37
37
|
harnessOp: op,
|
|
38
38
|
verdict: verdict ?? null,
|
|
39
|
-
|
|
39
|
+
// Covers: req~harness-state~1
|
|
40
|
+
openQuestions: repeated(flags.question),
|
|
40
41
|
pauseForQuestions: Boolean(flags["pause-questions"]),
|
|
41
42
|
note: typeof flags.note === "string" ? flags.note : undefined,
|
|
42
43
|
});
|
|
@@ -5,7 +5,7 @@ import { scaffoldFeature } from "../../modules/lawbook/scaffold-change.js";
|
|
|
5
5
|
import { investigate, formatInvestigateResult } from "../../modules/lawbook/investigate.js";
|
|
6
6
|
import { handleHarness } from "../../modules/cortex/harness.js";
|
|
7
7
|
import { printHarnessWarnings } from "./cortex.js";
|
|
8
|
-
import { list } from "../lib/args.js";
|
|
8
|
+
import { list, repeated } from "../lib/args.js";
|
|
9
9
|
import { ui } from "../lib/ui.js";
|
|
10
10
|
function today() {
|
|
11
11
|
// The MCP path passes the date in; the CLL runs on a real machine, so read it here.
|
|
@@ -126,7 +126,10 @@ export async function runSpec(flags) {
|
|
|
126
126
|
console.log(JSON.stringify(result, null, 2));
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
|
-
ui.ok(`bug change "${name}" scaffolded at ${result.dir}`
|
|
129
|
+
ui.ok(`bug change "${name}" scaffolded at ${result.dir} ` +
|
|
130
|
+
(level === undefined
|
|
131
|
+
? "(level unconfirmed — run `speclaw lawbook level set`)"
|
|
132
|
+
: `(level ${level})`));
|
|
130
133
|
return;
|
|
131
134
|
}
|
|
132
135
|
const name = req(change, "lawbook draft <name> [--level N] [--capability C] [--json] | --bug <name>");
|
|
@@ -181,7 +184,8 @@ export async function runSpec(flags) {
|
|
|
181
184
|
change: changeName,
|
|
182
185
|
harnessOp: op,
|
|
183
186
|
verdict: verdict ?? null,
|
|
184
|
-
|
|
187
|
+
// Covers: req~harness-state~1
|
|
188
|
+
openQuestions: repeated(flags.question),
|
|
185
189
|
pauseForQuestions: Boolean(flags["pause-questions"]),
|
|
186
190
|
note: typeof flags.note === "string" ? flags.note : undefined,
|
|
187
191
|
});
|
|
@@ -73,13 +73,21 @@ export async function runLaws(flags) {
|
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
if (sub === "scan") {
|
|
76
|
+
// Text and --json share one exit rule: 1 on an unreadable lock or any
|
|
77
|
+
// error-severity finding, else 0.
|
|
78
|
+
// Covers: req~injection-scan~1, req~laws-integrity-cli~1
|
|
76
79
|
const report = verifyIntegrity({ projectPath: process.cwd(), checks: "scan" });
|
|
80
|
+
const failed = report.lockError !== undefined || report.findings.some((f) => f.severity === "error");
|
|
77
81
|
if (flags.json) {
|
|
78
82
|
console.log(JSON.stringify(report, null, 2));
|
|
83
|
+
if (failed)
|
|
84
|
+
process.exit(1);
|
|
79
85
|
return;
|
|
80
86
|
}
|
|
81
87
|
ui.heading("speclaw laws scan");
|
|
82
|
-
if (report.
|
|
88
|
+
if (report.lockError !== undefined)
|
|
89
|
+
ui.err(report.lockError);
|
|
90
|
+
if (report.findings.length === 0 && report.lockError === undefined) {
|
|
83
91
|
ui.ok("No injection findings.");
|
|
84
92
|
return;
|
|
85
93
|
}
|
|
@@ -91,7 +99,7 @@ export async function runLaws(flags) {
|
|
|
91
99
|
else
|
|
92
100
|
ui.warn(msg);
|
|
93
101
|
}
|
|
94
|
-
if (
|
|
102
|
+
if (failed)
|
|
95
103
|
process.exit(1);
|
|
96
104
|
return;
|
|
97
105
|
}
|
|
@@ -36,7 +36,7 @@ export async function runQuery(cmd, flags) {
|
|
|
36
36
|
try {
|
|
37
37
|
switch (cmd) {
|
|
38
38
|
case "explore": {
|
|
39
|
-
const r = explore(cwd, need(args[0], "explore <node>"));
|
|
39
|
+
const r = explore(cwd, need(args[0], "explore <node>"), { includeRefs: true });
|
|
40
40
|
if (!r.found) {
|
|
41
41
|
ui.warn(r.message ?? "not found");
|
|
42
42
|
r.otherMatches?.forEach((m) => ui.info(`${m.name} (${m.kind}) ${m.file}:${m.line}`));
|
|
@@ -51,7 +51,7 @@ export async function runQuery(cmd, flags) {
|
|
|
51
51
|
ui.info(`+ ${r.unresolvedCallees.count} unresolved call(s): ${r.unresolvedCallees.sample.join(", ")}`);
|
|
52
52
|
}
|
|
53
53
|
ui.heading("Callers");
|
|
54
|
-
r.callers?.forEach((c) => ui.info(`${c.kind === "file" ? "(file) " : ""}${c.name} (${c.file}:${c.line})`));
|
|
54
|
+
r.callers?.forEach((c) => ui.info(`${c.kind === "file" ? "(file) " : ""}${c.name} (${c.file}:${c.line})${c.via ? ` via ${c.via}` : ""}`));
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
case "search":
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { shipOnStop } from "../../modules/lawbook/ship.js";
|
|
2
|
+
/**
|
|
3
|
+
* `speclaw ship-on-stop`: the Claude Code `Stop` hook. Ships the branch's
|
|
4
|
+
* change when the work changed since the last ship; prints nothing on stdout.
|
|
5
|
+
* A failing gate goes to stderr with exit 2 so the agent sees it and fixes it
|
|
6
|
+
* — once: when the hook already blocked this stop (`stop_hook_active`), it
|
|
7
|
+
* exits 0 so the agent can never loop. Any other failure exits 0.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd - Project root (defaults to the process working directory).
|
|
10
|
+
*/
|
|
11
|
+
// Covers: req~ship-on-stop-hook~1
|
|
12
|
+
export async function runShipOnStop(cwd = process.cwd()) {
|
|
13
|
+
const active = await stopHookActive();
|
|
14
|
+
try {
|
|
15
|
+
const out = shipOnStop(cwd);
|
|
16
|
+
if (out.skipped === null && !out.result.gatesPassed && !active) {
|
|
17
|
+
const failed = out.result.gates[out.result.gates.length - 1];
|
|
18
|
+
process.stderr.write(`speclaw: gate failed — ${failed.command} (exit ${failed.exitCode}). Fix it before finishing.\n${failed.tail}\n`);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// A hook must never break the session.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Whether this stop was already blocked by the hook once (`stop_hook_active`). */
|
|
27
|
+
async function stopHookActive() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = process.stdin.isTTY ? "" : await readStdin();
|
|
30
|
+
return raw
|
|
31
|
+
? Boolean(JSON.parse(raw).stop_hook_active)
|
|
32
|
+
: false;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function readStdin() {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let data = "";
|
|
41
|
+
process.stdin.setEncoding("utf8");
|
|
42
|
+
process.stdin.on("data", (c) => (data += c));
|
|
43
|
+
process.stdin.on("end", () => resolve(data));
|
|
44
|
+
setTimeout(() => resolve(data), 1000).unref();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { list } from "../lib/args.js";
|
|
2
|
+
import { ui } from "../lib/ui.js";
|
|
3
|
+
import { shipChange } from "../../modules/lawbook/ship.js";
|
|
4
|
+
/**
|
|
5
|
+
* Fast path for finished work (`speclaw ship <name>`): gates once, a report
|
|
6
|
+
* from their real output, and the archive at level 0.
|
|
7
|
+
*
|
|
8
|
+
* @param flags - `_[0]` is the change name; `--summary`, `--gate`, `--discipline`, `--no-archive`, `--json`.
|
|
9
|
+
*/
|
|
10
|
+
export async function runShip(flags) {
|
|
11
|
+
const name = flags._[0];
|
|
12
|
+
if (!name || typeof name !== "string") {
|
|
13
|
+
ui.err("Usage: speclaw ship <name> [--summary <text>] [--gate <cmd>] [--discipline <name>]");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const gates = list(flags.gate);
|
|
17
|
+
const result = shipChange(process.cwd(), name, {
|
|
18
|
+
summary: typeof flags.summary === "string" ? flags.summary : undefined,
|
|
19
|
+
gates: gates.length ? gates : undefined,
|
|
20
|
+
discipline: typeof flags.discipline === "string" ? flags.discipline : undefined,
|
|
21
|
+
noArchive: Boolean(flags["no-archive"]),
|
|
22
|
+
});
|
|
23
|
+
if (flags.json) {
|
|
24
|
+
console.log(JSON.stringify(result, null, 2));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const g of result.gates) {
|
|
28
|
+
const t = `${(g.durationMs / 1000).toFixed(1)} s`;
|
|
29
|
+
if (g.exitCode === 0)
|
|
30
|
+
ui.ok(`${g.command} (${t})`);
|
|
31
|
+
else
|
|
32
|
+
ui.err(`${g.command} failed, exit ${g.exitCode} (${t})`);
|
|
33
|
+
}
|
|
34
|
+
ui.ok(`report ${ui.code(result.report)}`);
|
|
35
|
+
if (result.archivedTo)
|
|
36
|
+
ui.ok(`archived to ${result.archivedTo}`);
|
|
37
|
+
for (const n of result.next)
|
|
38
|
+
ui.warn(n);
|
|
39
|
+
const s = (ms) => `${(ms / 1000).toFixed(2)} s`;
|
|
40
|
+
ui.info(`speclaw overhead ${s(result.timings.overhead)} · gates ${s(result.timings.gates)} · total ${s(result.timings.total)}`);
|
|
41
|
+
}
|
|
42
|
+
if (!result.gatesPassed)
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
@@ -269,6 +269,31 @@ const MIGRATIONS = [
|
|
|
269
269
|
"up at the next session start or `compass_index`.\n" +
|
|
270
270
|
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
271
271
|
},
|
|
272
|
+
{
|
|
273
|
+
version: "2.0.13",
|
|
274
|
+
describe: "Compass schema 12 — type-reference callers, compact compass_find, unconfirmed bug drafts",
|
|
275
|
+
agentPrompt: "- Compass is now **schema 12**: the first `speclaw index` after upgrading migrates " +
|
|
276
|
+
"11→12 and re-extracts every file once (embeddings are reused). Where the project " +
|
|
277
|
+
'mentions "Schema **11**" or "Compass (schema 11)", change it to schema 12 ' +
|
|
278
|
+
"(11→12 forces a reindex; embeddings reused).\n" +
|
|
279
|
+
"- speclaw 2.0.12 or older (including an MCP entry pinned at `@2.0.12` or a stale " +
|
|
280
|
+
"global CLI) treats a schema-12 index as incompatible and rebuilds it from scratch. " +
|
|
281
|
+
"Pin every speclaw the project uses (global CLI, `npx`, CI, and the MCP entry) to " +
|
|
282
|
+
"2.0.13: run `speclaw update` so the pinned MCP entry moves to 2.0.13.\n" +
|
|
283
|
+
"- `compass_find` now returns one compact JSON document capped as a whole at " +
|
|
284
|
+
"`maxTokens` (default 1500): hits carry only `name`, `kind`, `file`, `line`; exact mode " +
|
|
285
|
+
"adds `found`, `terms`, and `nearest` when no symbol has the name; `focusIgnored` lists " +
|
|
286
|
+
"focus paths that are not indexed; `truncated: true` appears only when it trimmed. " +
|
|
287
|
+
"`speclaw search --json` keeps its shape. If the project's docs describe the find " +
|
|
288
|
+
"response fields, update them.\n" +
|
|
289
|
+
"- `compass_explore` and `speclaw explore` list type references (`ref` edges: " +
|
|
290
|
+
"annotations, `extends`/`implements`) among callers; every caller carries " +
|
|
291
|
+
'`via: "call" | "ref"`. Blast radius, affected tests, and ranking are unchanged.\n' +
|
|
292
|
+
"- Cortex: `pauseForQuestions` is accepted only from stage `planning` (an error " +
|
|
293
|
+
"elsewhere); `advance`/`rework` re-read the confirmed level; `speclaw lawbook draft " +
|
|
294
|
+
"--bug` without `--level` leaves the level unconfirmed (validate uses 3).\n" +
|
|
295
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
296
|
+
},
|
|
272
297
|
];
|
|
273
298
|
/**
|
|
274
299
|
* Bring the current project up to date without a full re-init. When the npm
|
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseFlags } from "./lib/args.js";
|
|
2
|
+
import { parseFlags, REPEATABLE_FLAGS } from "./lib/args.js";
|
|
3
3
|
import { ui, header } from "./lib/ui.js";
|
|
4
4
|
import { maybeNotifyUpdate } from "./lib/update-check.js";
|
|
5
5
|
import { GLOBAL_HELP as HELP, helpFor, knownCommands, wantsHelp } from "./lib/help.js";
|
|
@@ -28,6 +28,7 @@ const HEADER_COMMANDS = new Set([
|
|
|
28
28
|
"lawbook",
|
|
29
29
|
"cortex",
|
|
30
30
|
"quick",
|
|
31
|
+
"ship",
|
|
31
32
|
]);
|
|
32
33
|
/**
|
|
33
34
|
* Print the branded header once, ahead of a command's output, when it is a
|
|
@@ -124,6 +125,10 @@ async function dispatch(cmd, flags) {
|
|
|
124
125
|
return (await import("./commands/visualize.js")).runVisualize(flags);
|
|
125
126
|
case "quick":
|
|
126
127
|
return (await import("./commands/quick.js")).runQuick(flags);
|
|
128
|
+
case "ship":
|
|
129
|
+
return (await import("./commands/ship.js")).runShip(flags);
|
|
130
|
+
case "ship-on-stop":
|
|
131
|
+
return (await import("./commands/ship-on-stop.js")).runShipOnStop();
|
|
127
132
|
case "cortex":
|
|
128
133
|
return (await import("./commands/cortex.js")).runCortex(flags);
|
|
129
134
|
case "lawbook":
|
|
@@ -166,11 +171,11 @@ async function main() {
|
|
|
166
171
|
process.exitCode = 0;
|
|
167
172
|
return;
|
|
168
173
|
}
|
|
169
|
-
const flags = parseFlags(rest);
|
|
174
|
+
const flags = parseFlags(rest, REPEATABLE_FLAGS);
|
|
170
175
|
maybeHeader(cmd, flags);
|
|
171
176
|
await dispatch(cmd, flags);
|
|
172
177
|
// The SessionStart and edit hooks must stay silent: no update notice either.
|
|
173
|
-
if (cmd === "session-start" || cmd === "reindex-file")
|
|
178
|
+
if (cmd === "session-start" || cmd === "reindex-file" || cmd === "ship-on-stop")
|
|
174
179
|
return;
|
|
175
180
|
await maybeNotifyUpdate(cmd);
|
|
176
181
|
}
|
package/dist/cli/lib/args.js
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal flag parser: `--key value`, `--key=value`, `--bool`, `-x`.
|
|
2
|
+
* Minimal flag parser: `--key value`, `--key=value`, `--bool`, `-x`. A repeated
|
|
3
|
+
* key keeps its last value unless it is listed in `repeatable`.
|
|
3
4
|
*
|
|
4
5
|
* @param argv - Raw argument tokens (already stripped of the command name).
|
|
6
|
+
* @param repeatable - Keys whose every value is kept: such a key always yields a
|
|
7
|
+
* `string[]` in argv order, and its values are never split at commas.
|
|
5
8
|
* @returns Flags with named options set and non-flag tokens collected in `_`.
|
|
6
9
|
*/
|
|
7
|
-
export function parseFlags(argv) {
|
|
10
|
+
export function parseFlags(argv, repeatable = []) {
|
|
8
11
|
const flags = { _: [] };
|
|
12
|
+
const set = (key, value) => {
|
|
13
|
+
const prev = flags[key];
|
|
14
|
+
if (repeatable.includes(key) && (typeof value === "string" || Array.isArray(prev))) {
|
|
15
|
+
// A bare repeatable flag after real values adds nothing and drops nothing.
|
|
16
|
+
if (typeof value === "string")
|
|
17
|
+
flags[key] = Array.isArray(prev) ? [...prev, value] : [value];
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
flags[key] = value;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
9
23
|
for (let i = 0; i < argv.length; i++) {
|
|
10
24
|
const arg = argv[i];
|
|
11
25
|
if (arg.startsWith("--")) {
|
|
12
26
|
const [key, inlineVal] = arg.slice(2).split("=", 2);
|
|
13
27
|
if (inlineVal !== undefined) {
|
|
14
|
-
|
|
28
|
+
set(key, inlineVal);
|
|
15
29
|
}
|
|
16
30
|
else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
|
|
17
|
-
|
|
31
|
+
set(key, argv[++i]);
|
|
18
32
|
}
|
|
19
33
|
else {
|
|
20
|
-
|
|
34
|
+
set(key, true);
|
|
21
35
|
}
|
|
22
36
|
}
|
|
23
37
|
else if (arg.startsWith("-")) {
|
|
@@ -29,6 +43,22 @@ export function parseFlags(argv) {
|
|
|
29
43
|
}
|
|
30
44
|
return flags;
|
|
31
45
|
}
|
|
46
|
+
/** Flags that may be passed more than once; each occurrence is one value. */
|
|
47
|
+
export const REPEATABLE_FLAGS = ["question"];
|
|
48
|
+
/**
|
|
49
|
+
* The values of a repeatable flag parsed with {@link REPEATABLE_FLAGS}, as-is:
|
|
50
|
+
* in argv order and never split at commas.
|
|
51
|
+
*
|
|
52
|
+
* @param value - The parsed flag value.
|
|
53
|
+
* @returns The string values; empty when the flag is absent or a bare boolean.
|
|
54
|
+
*/
|
|
55
|
+
export function repeated(value) {
|
|
56
|
+
if (Array.isArray(value))
|
|
57
|
+
return value;
|
|
58
|
+
if (typeof value === "string")
|
|
59
|
+
return [value];
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
32
62
|
/**
|
|
33
63
|
* Normalize a flag value into a list, splitting comma-separated strings.
|
|
34
64
|
*
|
package/dist/cli/lib/help.js
CHANGED
|
@@ -24,6 +24,7 @@ Setup
|
|
|
24
24
|
Compass (code intelligence — the same surface agents use via MCP)
|
|
25
25
|
index (Re)build the local code graph (--force / --prune / --json)
|
|
26
26
|
session-start Silent, fail-safe refresh of an existing index (SessionStart hook)
|
|
27
|
+
ship-on-stop Ship the branch's change when work changed (Stop hook)
|
|
27
28
|
reindex-file [paths...] Silent re-index of edited files (PostToolUse hook; stdin JSON)
|
|
28
29
|
watch Keep the index fresh on file changes
|
|
29
30
|
explore <node> A node's source + callers/callees
|
|
@@ -41,6 +42,7 @@ Cortex (One brain. Many agents. — multi-agent loop)
|
|
|
41
42
|
cortex <op> status|start|advance|rework|brief — drive harness.json (--change)
|
|
42
43
|
|
|
43
44
|
Lawbook (spec-driven workflow)
|
|
45
|
+
ship <name> Fast path: gates once → report → archive (level 0)
|
|
44
46
|
quick <name> Scaffold a level-0 change (record.md + reports)
|
|
45
47
|
lawbook init Create the lawbook/ workspace
|
|
46
48
|
lawbook list Active/archived changes and capabilities
|
|
@@ -164,6 +166,15 @@ Options
|
|
|
164
166
|
usage: `Usage: speclaw watch
|
|
165
167
|
|
|
166
168
|
Keep the Compass index fresh: re-index changed files until interrupted (Ctrl-C).
|
|
169
|
+
`,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "ship-on-stop",
|
|
173
|
+
usage: `Usage: speclaw ship-on-stop
|
|
174
|
+
|
|
175
|
+
The Claude Code Stop hook. On a feature branch whose work changed since the
|
|
176
|
+
last ship, runs \`speclaw ship\` for the change named after the branch. Prints
|
|
177
|
+
nothing on stdout; a failing gate goes to stderr with exit 2 once.
|
|
167
178
|
`,
|
|
168
179
|
},
|
|
169
180
|
{
|
|
@@ -196,7 +207,9 @@ speclaw index, compass_index, watch). Prints nothing and always exits 0.
|
|
|
196
207
|
name: "explore",
|
|
197
208
|
usage: `Usage: speclaw explore <node> [options]
|
|
198
209
|
|
|
199
|
-
A symbol's source plus its callers and callees.
|
|
210
|
+
A symbol's source plus its callers and callees. Each caller carries
|
|
211
|
+
via: call (it calls the symbol) or ref (it names the type in an annotation
|
|
212
|
+
or an extends/implements clause).
|
|
200
213
|
|
|
201
214
|
Options
|
|
202
215
|
--json Print the result as JSON
|
|
@@ -307,6 +320,23 @@ Options
|
|
|
307
320
|
--depth <n> Neighborhood depth around <node>
|
|
308
321
|
--limit <n> Maximum nodes
|
|
309
322
|
--no-open Do not open the browser
|
|
323
|
+
`,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "ship",
|
|
327
|
+
usage: `Usage: speclaw ship <name> [options]
|
|
328
|
+
|
|
329
|
+
Fast path for finished work: scaffold a level-0 record if the change is new,
|
|
330
|
+
run the project's gates once, write reports/<discipline>.md from their real
|
|
331
|
+
output, and archive at level 0. Level 1+ stops after the evidence: review
|
|
332
|
+
happens on the PR. Ship never records a review verdict.
|
|
333
|
+
|
|
334
|
+
Options
|
|
335
|
+
--summary <text> What changed (default: the changed-file list)
|
|
336
|
+
--gate <cmd> Gate command (repeatable; default ship.gates or package.json)
|
|
337
|
+
--discipline <name> Report name (default ship.discipline or "change")
|
|
338
|
+
--no-archive Stop after the report
|
|
339
|
+
--json Print the result as JSON
|
|
310
340
|
`,
|
|
311
341
|
},
|
|
312
342
|
{
|
|
@@ -331,8 +361,9 @@ Options
|
|
|
331
361
|
--change <name> The change (or pass it as the second argument)
|
|
332
362
|
--verdict <v> PASS or FAIL (advance from reviewing/testing)
|
|
333
363
|
--note <text> A history note
|
|
334
|
-
--question <text>
|
|
335
|
-
--pause-questions
|
|
364
|
+
--question <text> One open question, commas included; repeat for more
|
|
365
|
+
--pause-questions From planning only: pause for the --question list
|
|
366
|
+
(any other stage is rejected, exit 1)
|
|
336
367
|
--json Print the result as JSON
|
|
337
368
|
`,
|
|
338
369
|
},
|
|
@@ -344,7 +375,8 @@ Options
|
|
|
344
375
|
list Active/archived changes and capabilities
|
|
345
376
|
level <mode> [change] propose|set|promote|explain ceremony level (--level, --reason)
|
|
346
377
|
draft <name> Scaffold a feature change (--level N, --capability C)
|
|
347
|
-
draft --bug <name> Scaffold a bug change (bugfix.md + reports
|
|
378
|
+
draft --bug <name> Scaffold a bug change (bugfix.md + reports;
|
|
379
|
+
level unconfirmed unless --level N)
|
|
348
380
|
investigate Rank bug suspects (--symptom, --stack-trace, --symbol, --path)
|
|
349
381
|
validate <change> Validate a change's artifacts
|
|
350
382
|
sync <change> Promote delta specs to canonical
|
|
@@ -234,7 +234,7 @@ export function probeFts5Support() {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
/** Schema version stamped into the `meta` table on first creation. */
|
|
237
|
-
export const SCHEMA_VERSION = "
|
|
237
|
+
export const SCHEMA_VERSION = "12";
|
|
238
238
|
/**
|
|
239
239
|
* `nodes.kind` of the synthetic per-file owner node. It owns the references that
|
|
240
240
|
* sit outside any definition (top-level imports, calls in callbacks) and is
|
|
@@ -253,7 +253,7 @@ function readSchemaVersion(db) {
|
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
255
|
* Decide whether an existing database is from an incompatible schema and must be
|
|
256
|
-
* rebuilt. Schema 8→9, 9→10,
|
|
256
|
+
* rebuilt. Schema 8→9, 9→10, 10→11, and 11→12 are handled by migrators instead of a wipe.
|
|
257
257
|
*/
|
|
258
258
|
function isStale(db) {
|
|
259
259
|
const hasEdges = db
|
|
@@ -262,10 +262,8 @@ function isStale(db) {
|
|
|
262
262
|
if (!hasEdges)
|
|
263
263
|
return false;
|
|
264
264
|
const ver = readSchemaVersion(db);
|
|
265
|
-
if (ver === "8" || ver === "9" || ver === "10")
|
|
265
|
+
if (ver === "8" || ver === "9" || ver === "10" || ver === "11")
|
|
266
266
|
return false; // migrate in openDb
|
|
267
|
-
if (ver === "11" && !hasEdgeSpec(db))
|
|
268
|
-
return false; // pre-release 11: migrate in openDb
|
|
269
267
|
if (ver !== SCHEMA_VERSION)
|
|
270
268
|
return true;
|
|
271
269
|
const edgeCols = db.prepare("PRAGMA table_info(edges)").all().map((c) => c.name);
|
|
@@ -469,12 +467,49 @@ export function migrate10to11(db) {
|
|
|
469
467
|
throw new Error(`schema 10→11 migration failed — delete .speclaw/index.db to rebuild: ${err.message}`, { cause: err });
|
|
470
468
|
}
|
|
471
469
|
}
|
|
470
|
+
/** Reason recorded with the needs-reindex marker by the 11→12 migration. */
|
|
471
|
+
export const SCHEMA_12_REINDEX_REASON = "schema 12 adds type-reference (ref) edges for explore callers; reindex required";
|
|
472
|
+
/**
|
|
473
|
+
* Migrate schema 11 → 12: set the needs-reindex marker (its reason names
|
|
474
|
+
* schema 12, after any reason an earlier step of the same chain recorded) and
|
|
475
|
+
* stamp `"12"`, in one `BEGIN IMMEDIATE` transaction. No column changes
|
|
476
|
+
* (`edges.kind` is text); the next index run re-extracts every file so `ref`
|
|
477
|
+
* edges exist, while `embedding_cache` is untouched so unchanged symbols reuse
|
|
478
|
+
* their vectors.
|
|
479
|
+
*
|
|
480
|
+
* @param db - Open connection already at schema 11 (with `edges.spec`).
|
|
481
|
+
* @throws If any step fails; the transaction is rolled back and the stamp stays `"11"`.
|
|
482
|
+
*/
|
|
483
|
+
export function migrate11to12(db) {
|
|
484
|
+
// Covers: req~schema-ref-edges~1
|
|
485
|
+
db.exec("BEGIN IMMEDIATE");
|
|
486
|
+
try {
|
|
487
|
+
const pending = db.prepare("SELECT value FROM meta WHERE key = 'needs_reindex'").get();
|
|
488
|
+
const prior = db.prepare("SELECT value FROM meta WHERE key = 'reindex_reason'").get();
|
|
489
|
+
const reason = pending?.value === "1" && prior?.value && prior.value !== SCHEMA_12_REINDEX_REASON
|
|
490
|
+
? `${prior.value}; ${SCHEMA_12_REINDEX_REASON}`
|
|
491
|
+
: SCHEMA_12_REINDEX_REASON;
|
|
492
|
+
db.prepare("INSERT INTO meta(key, value) VALUES ('needs_reindex', '1') ON CONFLICT(key) DO UPDATE SET value = excluded.value").run();
|
|
493
|
+
db.prepare("INSERT INTO meta(key, value) VALUES ('reindex_reason', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(reason);
|
|
494
|
+
db.prepare("INSERT INTO meta(key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run("12");
|
|
495
|
+
db.exec("COMMIT");
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
try {
|
|
499
|
+
db.exec("ROLLBACK");
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
/* already rolled back */
|
|
503
|
+
}
|
|
504
|
+
throw new Error(`schema 11→12 migration failed — delete .speclaw/index.db to rebuild: ${err.message}`, { cause: err });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
472
507
|
/**
|
|
473
508
|
* Open (creating if needed) the index database at `<projectPath>/.speclaw/index.db`.
|
|
474
509
|
*
|
|
475
510
|
* Ensures the `.speclaw` directory exists, enables WAL journaling and foreign
|
|
476
|
-
* keys, and applies the schema. Schema 8→9, 9→10,
|
|
477
|
-
* and chain forward (embeddings preserved). Other incompatible schemas are wiped
|
|
511
|
+
* keys, and applies the schema. Schema 8→9, 9→10, 10→11, and 11→12 migrate in
|
|
512
|
+
* place and chain forward (embeddings preserved). Other incompatible schemas are wiped
|
|
478
513
|
* and rebuilt.
|
|
479
514
|
*
|
|
480
515
|
* @param projectPath - Absolute path to the project root.
|
|
@@ -494,13 +529,16 @@ export function openDb(projectPath) {
|
|
|
494
529
|
return null;
|
|
495
530
|
}
|
|
496
531
|
})();
|
|
497
|
-
if (ver === "8" || ver === "9" || ver === "10" ||
|
|
532
|
+
if (ver === "8" || ver === "9" || ver === "10" || ver === "11") {
|
|
498
533
|
try {
|
|
499
534
|
if (ver === "8")
|
|
500
535
|
migrate8to9(db, projectPath);
|
|
501
536
|
if (ver === "8" || ver === "9")
|
|
502
537
|
migrate9to10(db);
|
|
503
|
-
|
|
538
|
+
// A pre-release schema-11 index (no `edges.spec`) takes the 10→11 step too.
|
|
539
|
+
if (ver !== "11" || !hasEdgeSpec(db))
|
|
540
|
+
migrate10to11(db);
|
|
541
|
+
migrate11to12(db);
|
|
504
542
|
}
|
|
505
543
|
catch (err) {
|
|
506
544
|
db.close();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { explore, impact, trace } from "./query.js";
|
|
2
2
|
import { affectedTests } from "./affected.js";
|
|
3
|
-
import { FILE_NODE_KIND } from "./db.js";
|
|
3
|
+
import { FILE_NODE_KIND, indexExists, openDb } from "./db.js";
|
|
4
4
|
import { hotspots } from "./hotspots.js";
|
|
5
5
|
import { summarizeImpact } from "./impact-summary.js";
|
|
6
|
-
import { budgetExploreShape, applyTextBudget, } from "../../shared/output-budget.js";
|
|
6
|
+
import { budgetExploreShape, applyTextBudget, OUTPUT_BUDGET, } from "../../shared/output-budget.js";
|
|
7
7
|
const DEFAULT_INCLUDES = [
|
|
8
8
|
"source",
|
|
9
9
|
"callers",
|
|
@@ -28,7 +28,7 @@ export async function exploreRich(query) {
|
|
|
28
28
|
const degraded = [];
|
|
29
29
|
if (query.to) {
|
|
30
30
|
const pathResult = trace(query.projectPath, query.node, query.to, query.maxDepth ?? 8);
|
|
31
|
-
const base = explore(query.projectPath, query.node);
|
|
31
|
+
const base = explore(query.projectPath, query.node, { includeRefs: true });
|
|
32
32
|
const out = {
|
|
33
33
|
...base,
|
|
34
34
|
path: pathResult.path,
|
|
@@ -49,7 +49,7 @@ export async function exploreRich(query) {
|
|
|
49
49
|
budgetExploreShape(out, mode, truncated);
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
|
-
const base = explore(query.projectPath, query.node);
|
|
52
|
+
const base = explore(query.projectPath, query.node, { includeRefs: true });
|
|
53
53
|
const out = { ...base, truncated, degraded };
|
|
54
54
|
if (!includes.includes("source") && out.symbol)
|
|
55
55
|
out.symbol = withoutSource(out.symbol);
|
|
@@ -123,16 +123,57 @@ export async function exploreRich(query) {
|
|
|
123
123
|
delete out.degraded;
|
|
124
124
|
return out;
|
|
125
125
|
}
|
|
126
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Merge lexical and semantic search behind one surface (the MCP find path).
|
|
128
|
+
* The focus is resolved against the index first so dropped paths can be
|
|
129
|
+
* reported; exact mode keeps only symbols named exactly a query term and
|
|
130
|
+
* reports `found`, `terms`, and, when nothing matches, `nearest`.
|
|
131
|
+
*
|
|
132
|
+
* @param projectPath - Indexed project root.
|
|
133
|
+
* @param query - Identifier(s) or prose.
|
|
134
|
+
* @param mode - `exact` (name lookup) or `concept` (fuzzy, dense-heavy).
|
|
135
|
+
* @param limit - Seed limit (default 50).
|
|
136
|
+
* @param opts - Focus paths and the response token cap (default: brief ceiling).
|
|
137
|
+
*/
|
|
138
|
+
// Covers: req~find-exact-not-found~1, req~task-relative-ranking~1
|
|
127
139
|
export async function findSymbols(projectPath, query, mode, limit, opts) {
|
|
128
|
-
const { hybridSearch } = await import("./hybrid.js");
|
|
140
|
+
const { hybridSearch, resolveSearchFocus, exactTerms, nearestSymbols } = await import("./hybrid.js");
|
|
141
|
+
if (!indexExists(projectPath)) {
|
|
142
|
+
throw new Error("No index found. Build it first with the index_build tool (creates .speclaw/index.db).");
|
|
143
|
+
}
|
|
144
|
+
const db = openDb(projectPath);
|
|
145
|
+
let resolvedFocus;
|
|
146
|
+
try {
|
|
147
|
+
resolvedFocus = resolveSearchFocus(db, projectPath, opts?.focus);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
db.close();
|
|
151
|
+
}
|
|
152
|
+
const cap = opts?.maxTokens ?? OUTPUT_BUDGET.brief;
|
|
153
|
+
const terms = mode === "exact" ? exactTerms(query) : undefined;
|
|
154
|
+
const report = { ranked: 0, knnIds: [] };
|
|
129
155
|
const result = await hybridSearch(projectPath, query, {
|
|
130
156
|
mode,
|
|
131
|
-
|
|
132
|
-
maxTokens: opts?.maxTokens,
|
|
157
|
+
maxTokens: cap,
|
|
133
158
|
seedLimit: limit ?? 50,
|
|
159
|
+
resolvedFocus,
|
|
160
|
+
exactNames: terms,
|
|
161
|
+
report,
|
|
134
162
|
});
|
|
135
|
-
|
|
163
|
+
const out = {
|
|
164
|
+
...result,
|
|
165
|
+
mode,
|
|
166
|
+
focusIgnored: resolvedFocus.ignored,
|
|
167
|
+
cap,
|
|
168
|
+
capped: report.ranked > result.hits.length,
|
|
169
|
+
};
|
|
170
|
+
if (terms) {
|
|
171
|
+
out.terms = terms;
|
|
172
|
+
out.found = result.hits.length > 0;
|
|
173
|
+
if (!out.found)
|
|
174
|
+
out.nearest = nearestSymbols(projectPath, terms, report.knnIds);
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
136
177
|
}
|
|
137
178
|
/** Serialize explore-rich with output budget applied. */
|
|
138
179
|
export function formatExploreRich(result, mode = "brief") {
|