@esneiderbravo/speclaw 0.3.4 → 0.3.7
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 +66 -15
- package/dist/cli/commands/budget.js +36 -0
- package/dist/cli/commands/doctor.js +53 -11
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/commands/telemetry.js +16 -0
- package/dist/cli/commands/update.js +36 -3
- package/dist/cli/index.js +19 -4
- package/dist/modules/compass/indexer.js +10 -0
- package/dist/modules/compass/map.js +114 -0
- package/dist/modules/compass/register.js +30 -64
- package/dist/modules/foundation/assets/docs/compass.template.md +3 -0
- package/dist/modules/foundation/context-budget.js +58 -0
- package/dist/modules/foundation/doctor.js +626 -161
- package/dist/modules/foundation/graph.js +7 -4
- package/dist/modules/foundation/hooks.js +12 -0
- package/dist/modules/foundation/laws.js +1 -0
- package/dist/modules/foundation/register-core.js +108 -0
- package/dist/modules/foundation/register.js +22 -159
- package/dist/modules/foundation/scaffold.js +1 -1
- package/dist/modules/lawbook/assets/skills/archive/SKILL.md +1 -31
- package/dist/modules/lawbook/assets/skills/archive/steps/01-confirm-done.md +7 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/02-reconcile.md +15 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/03-validate-and-sync.md +7 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/04-archive.md +9 -0
- package/dist/modules/lawbook/assets/skills/archive/steps/05-report.md +7 -0
- package/dist/modules/lawbook/assets/skills/build/SKILL.md +2 -100
- package/dist/modules/lawbook/assets/skills/build/steps/01-load-change.md +7 -0
- package/dist/modules/lawbook/assets/skills/build/steps/02-branch.md +6 -0
- package/dist/modules/lawbook/assets/skills/build/steps/03-implement.md +11 -0
- package/dist/modules/lawbook/assets/skills/build/steps/04-quality-gates.md +10 -0
- package/dist/modules/lawbook/assets/skills/build/steps/05-manual-verification.md +22 -0
- package/dist/modules/lawbook/assets/skills/build/steps/06-discipline-reports.md +44 -0
- package/dist/modules/lawbook/assets/skills/build/steps/07-hand-off.md +9 -0
- package/dist/modules/lawbook/assets/skills/draft/SKILL.md +3 -80
- package/dist/modules/lawbook/assets/skills/draft/steps/01-ensure-workspace.md +5 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/02-understand.md +12 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/03-name-capabilities.md +14 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/04-write-artifacts.md +41 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/05-validate.md +11 -0
- package/dist/modules/lawbook/assets/skills/draft/steps/06-hand-off.md +5 -0
- package/dist/modules/lawbook/assets/skills/explore/SKILL.md +6 -22
- package/dist/modules/lawbook/assets/skills/explore/steps/01-investigate.md +16 -0
- package/dist/modules/lawbook/assets/skills/explore/steps/02-summarize.md +7 -0
- package/dist/modules/lawbook/assets/skills/sync/SKILL.md +1 -30
- package/dist/modules/lawbook/assets/skills/sync/steps/01-confirm.md +5 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/02-reconcile.md +16 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/03-validate.md +6 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/04-promote.md +10 -0
- package/dist/modules/lawbook/assets/skills/sync/steps/05-report.md +7 -0
- package/dist/modules/lawbook/register.js +17 -36
- package/dist/modules/tools/register.js +15 -15
- package/dist/server.js +13 -7
- package/dist/shared/budget.js +159 -0
- package/dist/shared/exposure.js +110 -0
- package/dist/shared/manifest.js +11 -2
- package/dist/shared/mcp.js +33 -0
- package/dist/shared/redact.js +90 -0
- package/dist/shared/schema-tokens.js +86 -0
- package/dist/shared/tokens.js +41 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
<br/>
|
|
6
6
|
|
|
7
7
|
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw"><img src="https://img.shields.io/npm/v/@esneiderbravo/speclaw?color=0E8E8E&labelColor=0B0F10&style=flat-square&label=npm" alt="npm"></a>
|
|
8
|
+
<a href="https://github.com/esneiderbravo/speclaw/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/esneiderbravo/speclaw/ci.yml?branch=main&label=CI&labelColor=0B0F10&style=flat-square&color=0E8E8E" alt="CI"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@esneiderbravo/speclaw?activeTab=versions"><img src="https://img.shields.io/badge/provenance-SLSA-0E8E8E?labelColor=0B0F10&style=flat-square" alt="npm provenance"></a>
|
|
8
10
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0E8E8E?labelColor=0B0F10&style=flat-square" alt="MIT"></a>
|
|
9
11
|
<img src="https://img.shields.io/badge/node-%E2%89%A522-0E8E8E?labelColor=0B0F10&style=flat-square" alt="Node >= 22">
|
|
10
12
|
|
|
@@ -29,32 +31,40 @@ One command. No cloud, no LLM, no API keys — <b>everything runs on your machin
|
|
|
29
31
|
<br/>
|
|
30
32
|
|
|
31
33
|
> [!TIP]
|
|
32
|
-
> **
|
|
33
|
-
> `speclaw init
|
|
34
|
-
>
|
|
34
|
+
> **One command. Detects your agents and wires only those.** Paste
|
|
35
|
+
> `npx @esneiderbravo/speclaw@latest init` — speclaw detects Claude Code, Cursor,
|
|
36
|
+
> Codex, Windsurf, and generic `AGENTS.md` surfaces, scaffolds the constitution +
|
|
37
|
+
> lawbook, indexes your code, and registers the local MCP server — only for the
|
|
38
|
+
> agents you pick. This one-liner is a **stable contract** (see
|
|
39
|
+
> [CONTRIBUTING.md](CONTRIBUTING.md)); do not invent alternate install commands in
|
|
40
|
+
> directories or newsletters.
|
|
35
41
|
|
|
36
42
|
<br/>
|
|
37
43
|
|
|
38
44
|
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> Quick start
|
|
39
45
|
|
|
40
|
-
Install speclaw globally (once), then run `init` in your project root:
|
|
41
|
-
|
|
42
46
|
```bash
|
|
43
|
-
|
|
44
|
-
speclaw init
|
|
47
|
+
npx @esneiderbravo/speclaw@latest init
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
<p align="center">
|
|
48
51
|
<img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-quickstart.png" width="800" alt="speclaw init">
|
|
49
52
|
</p>
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
Prefer a global install for repeated CLI use?
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm i -g @esneiderbravo/speclaw
|
|
58
|
+
speclaw init
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The `speclaw` command is then available everywhere — run `speclaw index`,
|
|
62
|
+
`speclaw doctor`, `speclaw visualize`, or `speclaw lawbook …` directly.
|
|
53
63
|
|
|
54
64
|
`init` will:
|
|
55
65
|
|
|
56
|
-
1. **Ask which agents you use** (Claude Code, Cursor, Codex, …) — and
|
|
57
|
-
only those. Add more later; nothing is forced on you.
|
|
66
|
+
1. **Ask which agents you use** (Claude Code, Cursor, Codex, Windsurf, …) — and
|
|
67
|
+
configure only those. Add more later; nothing is forced on you.
|
|
58
68
|
2. Write the **foundation** (constitution + standards) and the **lawbook workflow**,
|
|
59
69
|
and compile your blocking laws into **agent hooks** for the agents that support them.
|
|
60
70
|
3. **Index your code** with a live progress bar and a summary of what it found.
|
|
@@ -62,9 +72,23 @@ The `speclaw` command is now available everywhere — run `speclaw index`,
|
|
|
62
72
|
5. Print a prompt to paste into your agent so it fills the constitution with your
|
|
63
73
|
project's real architecture and conventions.
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
When something breaks, run `speclaw doctor --json` and paste it into an issue
|
|
76
|
+
(required on bug reports). Output is redacted by default.
|
|
77
|
+
|
|
78
|
+
<br/>
|
|
79
|
+
|
|
80
|
+
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> Verify a release (provenance)
|
|
81
|
+
|
|
82
|
+
Every npm publish is signed via **Trusted Publishing (OIDC)** and carries a
|
|
83
|
+
SLSA provenance attestation tied to this repository and workflow. That proves
|
|
84
|
+
*where* the tarball was built — not that its contents are benign. Pair it with
|
|
85
|
+
your own review and (later) law-integrity pinning.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npm audit signatures
|
|
89
|
+
# After downloading the tarball from the registry:
|
|
90
|
+
gh attestation verify <tarball> --owner esneiderbravo
|
|
91
|
+
```
|
|
68
92
|
|
|
69
93
|
<br/>
|
|
70
94
|
|
|
@@ -91,6 +115,30 @@ Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) an
|
|
|
91
115
|
|
|
92
116
|
<br/>
|
|
93
117
|
|
|
118
|
+
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> Context cost
|
|
119
|
+
|
|
120
|
+
speclaw publishes and **gates** its own always-on context cost. Measured with a
|
|
121
|
+
deterministic offline estimator (`speclaw/estimate-v1`, about ±8% vs Anthropic's
|
|
122
|
+
tokenizer on this corpus — not a BPE dependency):
|
|
123
|
+
|
|
124
|
+
| | Tokens |
|
|
125
|
+
| :-- | --: |
|
|
126
|
+
| **speclaw budget (always-on)** | **~11.7k** (budget ceiling **13.0k**) |
|
|
127
|
+
| Spec Kit commands alone | ~18.6k ([spec-kit#1401](https://github.com/github/spec-kit/issues/1401)) |
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
speclaw budget # human table
|
|
131
|
+
speclaw budget --json # machine-readable; used by the suite gate
|
|
132
|
+
speclaw init --minimal # omit setup/lifecycle MCP tools from registration
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Raising a number in committed `token-budget.json` is a reviewable PR. Optional
|
|
136
|
+
calibration (never CI): `npm run budget:calibrate` with `ANTHROPIC_API_KEY`.
|
|
137
|
+
MCP servers cannot mark tools `defer_loading` — savings come from shorter
|
|
138
|
+
definitions, omitted registration (`--minimal`), and JIT skill steps.
|
|
139
|
+
|
|
140
|
+
<br/>
|
|
141
|
+
|
|
94
142
|
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> The spec-driven workflow (Lawbook)
|
|
95
143
|
|
|
96
144
|
Lawbook is speclaw's answer to the biggest risk with AI agents: **code that
|
|
@@ -178,7 +226,10 @@ you** — commit your own skills and commands there if you want to.
|
|
|
178
226
|
|
|
179
227
|
**Enforcement artifacts.** For agents that support hooks, speclaw merges its law
|
|
180
228
|
hooks into that agent's settings (e.g. `.claude/settings.json`) **by identity** —
|
|
181
|
-
it never touches hooks you added yourself.
|
|
229
|
+
it never touches hooks you added yourself. Each speclaw `mcp_tool` hook includes
|
|
230
|
+
an `input` map Claude Code substitutes from the hook event (`${cwd}`,
|
|
231
|
+
`${hook_event_name}`, `${tool_input.file_path}`, …) so `speclaw_check` receives
|
|
232
|
+
`projectPath` / `event` / `payload`. The compiled law manifest lives in
|
|
182
233
|
`.speclaw/laws-manifest.json` (gitignored, regenerated on `init`/`update`), and a
|
|
183
234
|
context-coverage log in `.speclaw/context-log.jsonl` feeds `speclaw doctor`.
|
|
184
235
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ui } from "../lib/ui.js";
|
|
2
|
+
import { formatBudgetTable } from "../../shared/budget.js";
|
|
3
|
+
import { loadDeclaredBudget } from "../../shared/exposure.js";
|
|
4
|
+
import { measureInstallBudget } from "../../modules/foundation/context-budget.js";
|
|
5
|
+
/** Print the context-budget table or JSON. */
|
|
6
|
+
export async function runBudget(flags) {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const minimal = flags.minimal === true ? true : flags.minimal === false ? false : undefined;
|
|
9
|
+
const measurement = measureInstallBudget(cwd, minimal);
|
|
10
|
+
const declared = loadDeclaredBudget();
|
|
11
|
+
if (flags.json) {
|
|
12
|
+
console.log(JSON.stringify({
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
profile: measurement.profile,
|
|
15
|
+
tools: measurement.tools,
|
|
16
|
+
skillsAndCommands: measurement.skillsAndCommands,
|
|
17
|
+
alwaysOnInstructions: measurement.alwaysOnInstructions,
|
|
18
|
+
pathScoped: measurement.pathScoped,
|
|
19
|
+
total: measurement.total,
|
|
20
|
+
toolCount: measurement.toolCount,
|
|
21
|
+
declared: {
|
|
22
|
+
tools: declared.surfaces.tools,
|
|
23
|
+
skillsAndCommands: declared.surfaces.skillsAndCommands,
|
|
24
|
+
alwaysOnInstructions: declared.surfaces.alwaysOnInstructions,
|
|
25
|
+
total: measurement.profile === "minimal" ? declared.minimal.total : declared.total,
|
|
26
|
+
},
|
|
27
|
+
details: measurement.details,
|
|
28
|
+
}, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
ui.heading("speclaw budget");
|
|
32
|
+
console.log(formatBudgetTable(measurement, declared));
|
|
33
|
+
ui.plain();
|
|
34
|
+
ui.info(`profile: ${measurement.profile} · tools registered: ${measurement.toolCount}`);
|
|
35
|
+
ui.info("Spec Kit (commands only), for comparison: ~18,600 — github/spec-kit#1401");
|
|
36
|
+
}
|
|
@@ -1,21 +1,63 @@
|
|
|
1
1
|
import { doctor } from "../../modules/foundation/doctor.js";
|
|
2
2
|
import { ui } from "../lib/ui.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const STATUS_MARK = {
|
|
4
|
+
ok: "ok",
|
|
5
|
+
warn: "warn",
|
|
6
|
+
error: "error",
|
|
7
|
+
skip: "skip",
|
|
8
|
+
};
|
|
9
|
+
function printHuman(report) {
|
|
6
10
|
ui.heading("speclaw doctor");
|
|
7
|
-
for (const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
for (const section of report.sections) {
|
|
12
|
+
ui.plain();
|
|
13
|
+
ui.ok(`${section.id} [${STATUS_MARK[section.status]}]`);
|
|
14
|
+
for (const c of section.checks) {
|
|
15
|
+
const line = `${c.title.padEnd(24)} ${c.detail ?? c.status}`;
|
|
16
|
+
if (c.status === "ok" || c.status === "skip")
|
|
17
|
+
ui.ok(` ${line}`);
|
|
18
|
+
else if (c.status === "warn") {
|
|
19
|
+
ui.warn(` ${line}`);
|
|
20
|
+
if (c.remedy)
|
|
21
|
+
ui.plain(` → ${c.remedy}`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
ui.err(` ${line}`);
|
|
25
|
+
if (c.remedy)
|
|
26
|
+
ui.plain(` → ${c.remedy}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
12
29
|
}
|
|
13
|
-
const failed = checks.filter((c) => !c.ok).length;
|
|
14
30
|
ui.plain();
|
|
15
|
-
|
|
31
|
+
const warns = report.sections.flatMap((s) => s.checks).filter((c) => c.status === "warn").length;
|
|
32
|
+
const errs = report.sections.flatMap((s) => s.checks).filter((c) => c.status === "error").length;
|
|
33
|
+
if (errs === 0 && warns === 0)
|
|
16
34
|
ui.ok("Everything is within the law.");
|
|
35
|
+
else if (errs === 0) {
|
|
36
|
+
ui.warn(`${warns} warning(s). Run \`speclaw doctor --json\` and paste it into an issue.`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
ui.err(`${errs} error(s), ${warns} warning(s).`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Verify the installation. Supports `--json`, `--offline`, `--strict`,
|
|
44
|
+
* `--redact` (default) / `--no-redact`.
|
|
45
|
+
*/
|
|
46
|
+
export async function runDoctor(flags) {
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
const redact = flags["no-redact"] ? false : true;
|
|
49
|
+
const report = await doctor(cwd, {
|
|
50
|
+
offline: Boolean(flags.offline),
|
|
51
|
+
redact,
|
|
52
|
+
});
|
|
53
|
+
if (flags.json) {
|
|
54
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
55
|
+
}
|
|
17
56
|
else {
|
|
18
|
-
|
|
57
|
+
printHuman(report);
|
|
58
|
+
}
|
|
59
|
+
const strict = Boolean(flags.strict);
|
|
60
|
+
if (report.status === "error" || (strict && report.status === "warn")) {
|
|
19
61
|
process.exit(1);
|
|
20
62
|
}
|
|
21
63
|
}
|
|
@@ -87,7 +87,9 @@ export async function runInit(flags) {
|
|
|
87
87
|
if (reinit) {
|
|
88
88
|
ui.info("speclaw is already set up here — your existing files are kept; only missing pieces are added.");
|
|
89
89
|
}
|
|
90
|
-
const report = scaffold(cwd, profile, packs, agents
|
|
90
|
+
const report = scaffold(cwd, profile, packs, agents, {
|
|
91
|
+
minimal: Boolean(flags.minimal),
|
|
92
|
+
});
|
|
91
93
|
ui.ok(`Foundation ${c.muted("— LAWS.md + 8 standards + CLAUDE.md/AGENTS.md")}`);
|
|
92
94
|
ui.ok(`Lawbook workflow ${c.muted("— draft · build · sync · archive · explore")}`);
|
|
93
95
|
for (const p of packs)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ui } from "../lib/ui.js";
|
|
2
|
+
/**
|
|
3
|
+
* Telemetry posture command. speclaw ships **no** telemetry code — `status`
|
|
4
|
+
* says so; enable/disable/log are rejected.
|
|
5
|
+
*/
|
|
6
|
+
export async function runTelemetry(flags) {
|
|
7
|
+
const sub = flags._[0] ?? "status";
|
|
8
|
+
if (sub === "status") {
|
|
9
|
+
ui.ok("speclaw includes no telemetry — nothing is collected or transmitted.");
|
|
10
|
+
ui.plain("Policy: 100% local. There is no enable path and no analytics endpoint.");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
ui.err(`telemetry ${sub} is unavailable: speclaw does not include telemetry. ` +
|
|
14
|
+
"Run `speclaw telemetry status`.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
@@ -67,6 +67,31 @@ const MIGRATIONS = [
|
|
|
67
67
|
"required status check in branch protection. speclaw never enables that itself.\n" +
|
|
68
68
|
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
version: "0.3.5",
|
|
72
|
+
describe: "Context budget: compact map markers and budget-aware agent contracts",
|
|
73
|
+
agentPrompt: "- In docs/compass.md, ensure the markers `<!-- speclaw:map:start -->` and " +
|
|
74
|
+
"`<!-- speclaw:map:end -->` exist (usually just above 'Project-specific starting points') " +
|
|
75
|
+
"so `compass_index` can regenerate the compact project map between them. Do not hand-edit " +
|
|
76
|
+
"the content between the markers.\n" +
|
|
77
|
+
"- Optionally note in CLAUDE.md / AGENTS.md that `speclaw budget` reports always-on context " +
|
|
78
|
+
"cost and that `speclaw init --minimal` / `SPECLAW_MINIMAL=1` omit setup MCP tools " +
|
|
79
|
+
"(no server-side defer_loading).\n" +
|
|
80
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
version: "0.3.6",
|
|
84
|
+
describe: "Doctor JSON diagnostics and stable install one-liner",
|
|
85
|
+
agentPrompt: "- Mention that `speclaw doctor --json` is the support report (redacted by default) " +
|
|
86
|
+
"and that the stable install command is `npx @esneiderbravo/speclaw@latest init`.\n" +
|
|
87
|
+
"- Preserve all project-specific wording; only apply these speclaw-authored changes.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
version: "0.3.7",
|
|
91
|
+
describe: "Claude Code mcp_tool hooks pass speclaw_check input via ${…} templates",
|
|
92
|
+
// scaffold → installHooks already rewrites .claude/settings.json when the
|
|
93
|
+
// compiled hook shape changes; no extra run() step.
|
|
94
|
+
},
|
|
70
95
|
];
|
|
71
96
|
/**
|
|
72
97
|
* Update speclaw and bring the current project up to date without a full re-init:
|
|
@@ -108,7 +133,12 @@ export async function runUpdate(flags) {
|
|
|
108
133
|
// Re-exec the NEWLY installed binary so migrations run with the new assets
|
|
109
134
|
// and any new feature steps — not this (now-stale) process. Carry --backup
|
|
110
135
|
// through so the refresh honors it after the upgrade.
|
|
111
|
-
const reArgs = [
|
|
136
|
+
const reArgs = [
|
|
137
|
+
"update",
|
|
138
|
+
"--migrate-only",
|
|
139
|
+
...(backup ? ["--backup"] : []),
|
|
140
|
+
...(flags.minimal ? ["--minimal"] : []),
|
|
141
|
+
];
|
|
112
142
|
const re = spawnSync("speclaw", reArgs, {
|
|
113
143
|
stdio: "inherit",
|
|
114
144
|
shell: winShell,
|
|
@@ -125,7 +155,7 @@ export async function runUpdate(flags) {
|
|
|
125
155
|
return;
|
|
126
156
|
}
|
|
127
157
|
}
|
|
128
|
-
applyProjectMigrations(cwd, backup);
|
|
158
|
+
applyProjectMigrations(cwd, backup, flags.minimal ? true : undefined);
|
|
129
159
|
}
|
|
130
160
|
/**
|
|
131
161
|
* Additively apply the current version's content and feature steps to a project.
|
|
@@ -135,8 +165,10 @@ export async function runUpdate(flags) {
|
|
|
135
165
|
* @param backup - When true, a locally edited managed file is copied to
|
|
136
166
|
* `<file>.bak` before it is refreshed; the default overwrites it in place
|
|
137
167
|
* (recoverable from git) and only reports the overwrite.
|
|
168
|
+
* @param minimal - When true, persist minimal exposure; when undefined, keep
|
|
169
|
+
* the manifest's existing value.
|
|
138
170
|
*/
|
|
139
|
-
function applyProjectMigrations(cwd, backup) {
|
|
171
|
+
function applyProjectMigrations(cwd, backup, minimal) {
|
|
140
172
|
const initialized = fs.existsSync(path.join(cwd, "ai-specs")) || fs.existsSync(path.join(cwd, "LAWS.md"));
|
|
141
173
|
if (!initialized) {
|
|
142
174
|
ui.step("Project");
|
|
@@ -154,6 +186,7 @@ function applyProjectMigrations(cwd, backup) {
|
|
|
154
186
|
const report = scaffold(cwd, { project_name: detectProjectName(cwd) }, packs, agents, {
|
|
155
187
|
refreshManaged: true,
|
|
156
188
|
backup,
|
|
189
|
+
...(minimal !== undefined ? { minimal } : {}),
|
|
157
190
|
});
|
|
158
191
|
const changed = report.written.filter((w) => !w.includes(".gitignore"));
|
|
159
192
|
if (changed.length) {
|
package/dist/cli/index.js
CHANGED
|
@@ -11,7 +11,9 @@ Install globally so the command is always available:
|
|
|
11
11
|
|
|
12
12
|
Setup
|
|
13
13
|
init Interactive setup: pick agents, scaffold, index, get the prompt
|
|
14
|
+
(--minimal omits setup/lifecycle MCP tools)
|
|
14
15
|
update Upgrade speclaw and apply only what's new (no re-init)
|
|
16
|
+
(--minimal persists minimal exposure; omit to keep prior)
|
|
15
17
|
agent list Show which agents are configured
|
|
16
18
|
agent add <id> Configure another agent later (symlinks + MCP)
|
|
17
19
|
|
|
@@ -33,7 +35,9 @@ Lawbook (spec-driven workflow)
|
|
|
33
35
|
lawbook archive <c> Finalize and archive a change
|
|
34
36
|
|
|
35
37
|
Other
|
|
36
|
-
doctor Verify the installation
|
|
38
|
+
doctor Verify the installation (--json, --offline, --strict)
|
|
39
|
+
budget Measure always-on context cost (tools, skills, instructions)
|
|
40
|
+
telemetry status Confirm speclaw ships no telemetry
|
|
37
41
|
check Evaluate an action against the laws (hooks call this; --dry-run to preview)
|
|
38
42
|
laws verify Verify the deterministic dependency/graph laws against the index
|
|
39
43
|
verify Verify laws for CI: exit codes, --sarif, --json, --strict-engines
|
|
@@ -55,6 +59,8 @@ const HEADER_COMMANDS = new Set([
|
|
|
55
59
|
"update",
|
|
56
60
|
"agent",
|
|
57
61
|
"doctor",
|
|
62
|
+
"budget",
|
|
63
|
+
"telemetry",
|
|
58
64
|
"index",
|
|
59
65
|
"watch",
|
|
60
66
|
"lawbook",
|
|
@@ -64,13 +70,18 @@ const HEADER_COMMANDS = new Set([
|
|
|
64
70
|
* header-eligible command AND stdout is an interactive terminal (so pipes,
|
|
65
71
|
* redirection, and CI stay clean — mirroring the color gate in `ui.ts`). A
|
|
66
72
|
* forced-color signal counts as interactive so the header is exercisable in a
|
|
67
|
-
* child process.
|
|
73
|
+
* child process. `budget --json` and `doctor --json` are machine-consumed and
|
|
74
|
+
* suppress the header.
|
|
68
75
|
*/
|
|
69
|
-
function maybeHeader(cmd) {
|
|
76
|
+
function maybeHeader(cmd, flags) {
|
|
70
77
|
if (!process.stdout.isTTY && process.env.FORCE_COLOR !== "1")
|
|
71
78
|
return;
|
|
72
79
|
if (!HEADER_COMMANDS.has(cmd))
|
|
73
80
|
return;
|
|
81
|
+
if (cmd === "budget" && flags.json)
|
|
82
|
+
return;
|
|
83
|
+
if (cmd === "doctor" && flags.json)
|
|
84
|
+
return;
|
|
74
85
|
header();
|
|
75
86
|
}
|
|
76
87
|
/** Run the handler for a single command. Returns when the command completes. */
|
|
@@ -113,6 +124,10 @@ async function dispatch(cmd, flags) {
|
|
|
113
124
|
return (await import("./commands/lawbook.js")).runSpec(flags);
|
|
114
125
|
case "doctor":
|
|
115
126
|
return (await import("./commands/doctor.js")).runDoctor(flags);
|
|
127
|
+
case "budget":
|
|
128
|
+
return (await import("./commands/budget.js")).runBudget(flags);
|
|
129
|
+
case "telemetry":
|
|
130
|
+
return (await import("./commands/telemetry.js")).runTelemetry(flags);
|
|
116
131
|
case "check":
|
|
117
132
|
return (await import("./commands/check.js")).runCheck(flags);
|
|
118
133
|
case "laws":
|
|
@@ -129,7 +144,7 @@ async function dispatch(cmd, flags) {
|
|
|
129
144
|
async function main() {
|
|
130
145
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
131
146
|
const flags = parseFlags(rest);
|
|
132
|
-
maybeHeader(cmd);
|
|
147
|
+
maybeHeader(cmd, flags);
|
|
133
148
|
await dispatch(cmd, flags);
|
|
134
149
|
await maybeNotifyUpdate(cmd);
|
|
135
150
|
}
|
|
@@ -9,6 +9,7 @@ const SKIP_DIRS = new Set([
|
|
|
9
9
|
".git",
|
|
10
10
|
"node_modules",
|
|
11
11
|
"dist",
|
|
12
|
+
"dist-test",
|
|
12
13
|
"build",
|
|
13
14
|
".next",
|
|
14
15
|
"out",
|
|
@@ -164,6 +165,7 @@ export async function buildIndex(projectPath, onProgress) {
|
|
|
164
165
|
)
|
|
165
166
|
WHERE kind = 'call' AND dst_node_id IS NULL
|
|
166
167
|
`);
|
|
168
|
+
db.prepare("INSERT INTO meta(key, value) VALUES ('indexed_at', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(new Date().toISOString());
|
|
167
169
|
db.exec("COMMIT");
|
|
168
170
|
}
|
|
169
171
|
catch (err) {
|
|
@@ -173,5 +175,13 @@ export async function buildIndex(projectPath, onProgress) {
|
|
|
173
175
|
finally {
|
|
174
176
|
db.close();
|
|
175
177
|
}
|
|
178
|
+
// Compact map in committed docs/compass.md (between markers) — zero tool-call cost.
|
|
179
|
+
try {
|
|
180
|
+
const { writeCompactMap } = await import("./map.js");
|
|
181
|
+
writeCompactMap(projectPath);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Map generation must never fail an index run.
|
|
185
|
+
}
|
|
176
186
|
return stats;
|
|
177
187
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { openDb } from "./db.js";
|
|
4
|
+
import { estimateTokens } from "../../shared/tokens.js";
|
|
5
|
+
import { loadDeclaredBudget } from "../../shared/exposure.js";
|
|
6
|
+
export const MAP_START = "<!-- speclaw:map:start -->";
|
|
7
|
+
export const MAP_END = "<!-- speclaw:map:end -->";
|
|
8
|
+
/**
|
|
9
|
+
* Build a compact project map (hubs + top-level layout) from the Compass DB.
|
|
10
|
+
* Truncates to the declared map token budget.
|
|
11
|
+
*
|
|
12
|
+
* @param projectPath - Project root with `.speclaw/index.db`.
|
|
13
|
+
* @returns Map body text (no markers), or null if the index is missing/empty.
|
|
14
|
+
*/
|
|
15
|
+
export function generateCompactMap(projectPath) {
|
|
16
|
+
const dbPath = path.join(projectPath, ".speclaw", "index.db");
|
|
17
|
+
if (!fs.existsSync(dbPath))
|
|
18
|
+
return null;
|
|
19
|
+
const db = openDb(projectPath);
|
|
20
|
+
try {
|
|
21
|
+
const fileCount = db.prepare("SELECT COUNT(*) AS c FROM files").get().c;
|
|
22
|
+
const nodeCount = db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
|
|
23
|
+
if (fileCount === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const hubs = db
|
|
26
|
+
.prepare(`SELECT n.name AS name, COUNT(*) AS fan_in
|
|
27
|
+
FROM edges e
|
|
28
|
+
JOIN nodes n ON n.id = e.dst_node_id
|
|
29
|
+
WHERE e.kind = 'call' AND e.dst_node_id IS NOT NULL
|
|
30
|
+
GROUP BY n.id
|
|
31
|
+
ORDER BY fan_in DESC
|
|
32
|
+
LIMIT 12`)
|
|
33
|
+
.all();
|
|
34
|
+
const topDirs = db
|
|
35
|
+
.prepare(`SELECT CASE
|
|
36
|
+
WHEN instr(path, '/') > 0 THEN substr(path, 1, instr(path, '/') - 1)
|
|
37
|
+
ELSE path
|
|
38
|
+
END AS top, COUNT(*) AS c
|
|
39
|
+
FROM files
|
|
40
|
+
GROUP BY top
|
|
41
|
+
ORDER BY c DESC
|
|
42
|
+
LIMIT 8`)
|
|
43
|
+
.all();
|
|
44
|
+
const hubLine = hubs.length === 0
|
|
45
|
+
? "hubs: (none yet)"
|
|
46
|
+
: `hubs: ${hubs.map((h) => `${h.name} ${h.fan_in}`).join(" · ")}`;
|
|
47
|
+
const dirLine = topDirs.map((d) => `${d.top}/ (${d.c})`).join(" ");
|
|
48
|
+
let body = [
|
|
49
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
50
|
+
dirLine,
|
|
51
|
+
hubLine,
|
|
52
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
53
|
+
].join("\n");
|
|
54
|
+
const cap = loadDeclaredBudget().map;
|
|
55
|
+
let omitted = false;
|
|
56
|
+
while (estimateTokens(body) > cap && hubs.length > 3) {
|
|
57
|
+
hubs.pop();
|
|
58
|
+
omitted = true;
|
|
59
|
+
const shorter = hubs.length === 0
|
|
60
|
+
? "hubs: (truncated)"
|
|
61
|
+
: `hubs: ${hubs.map((h) => `${h.name} ${h.fan_in}`).join(" · ")}`;
|
|
62
|
+
body = [
|
|
63
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
64
|
+
dirLine,
|
|
65
|
+
shorter,
|
|
66
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
67
|
+
omitted ? "(entries omitted to fit map budget)" : "",
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
if (estimateTokens(body) > cap) {
|
|
73
|
+
body = [
|
|
74
|
+
`speclaw · ${fileCount} files · ${nodeCount} nodes`,
|
|
75
|
+
"entry: src/server.ts (mcp) · src/cli/index.ts (bin)",
|
|
76
|
+
"(entries omitted to fit map budget)",
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
db.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Replace content between map markers in `docs/compass.md`. Preserves content
|
|
87
|
+
* outside markers. Does nothing (with reason) when markers are missing or the
|
|
88
|
+
* map cannot be generated.
|
|
89
|
+
*
|
|
90
|
+
* @param projectPath - Project root.
|
|
91
|
+
*/
|
|
92
|
+
export function writeCompactMap(projectPath) {
|
|
93
|
+
const compassPath = path.join(projectPath, "docs", "compass.md");
|
|
94
|
+
if (!fs.existsSync(compassPath)) {
|
|
95
|
+
return { written: false, reason: "docs/compass.md missing" };
|
|
96
|
+
}
|
|
97
|
+
const original = fs.readFileSync(compassPath, "utf8");
|
|
98
|
+
const start = original.indexOf(MAP_START);
|
|
99
|
+
const end = original.indexOf(MAP_END);
|
|
100
|
+
if (start < 0 || end < 0 || end < start) {
|
|
101
|
+
return { written: false, reason: "map markers missing — not regenerating" };
|
|
102
|
+
}
|
|
103
|
+
const map = generateCompactMap(projectPath);
|
|
104
|
+
if (!map) {
|
|
105
|
+
// Leave markers but clear body so we don't leave a stale map.
|
|
106
|
+
const next = original.slice(0, start + MAP_START.length) + "\n" + original.slice(end);
|
|
107
|
+
if (next !== original)
|
|
108
|
+
fs.writeFileSync(compassPath, next);
|
|
109
|
+
return { written: false, reason: "no index or empty graph — map omitted" };
|
|
110
|
+
}
|
|
111
|
+
const next = original.slice(0, start + MAP_START.length) + "\n" + map + "\n" + original.slice(end);
|
|
112
|
+
fs.writeFileSync(compassPath, next);
|
|
113
|
+
return { written: true };
|
|
114
|
+
}
|