@esneiderbravo/speclaw 0.1.8 → 0.1.9
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 +37 -0
- package/dist/cli/commands/init.js +8 -4
- package/dist/cli/commands/lawbook.js +4 -2
- package/dist/cli/commands/update.js +8 -2
- package/dist/cli/commands/visualize.js +9 -3
- package/dist/cli/lib/args.js +4 -1
- package/dist/cli/lib/ui.js +1 -2
- package/dist/cli/lib/update-check.js +16 -4
- package/dist/modules/compass/db.js +1 -3
- package/dist/modules/compass/indexer.js +24 -5
- package/dist/modules/compass/query.js +5 -1
- package/dist/modules/compass/register.js +4 -2
- package/dist/modules/compass/watcher.js +11 -2
- package/dist/modules/foundation/doctor.js +2 -6
- package/dist/modules/foundation/register.js +66 -17
- package/dist/modules/lawbook/engine.js +14 -3
- package/dist/modules/lawbook/register.js +4 -1
- package/dist/shared/agents.js +34 -5
- package/dist/shared/manifest.js +4 -1
- package/dist/shared/version.js +4 -1
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -90,6 +90,43 @@ Compass is inspired by [CodeGraph](https://github.com/colbymchenry/codegraph) an
|
|
|
90
90
|
|
|
91
91
|
<br/>
|
|
92
92
|
|
|
93
|
+
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> The spec-driven workflow (Lawbook)
|
|
94
|
+
|
|
95
|
+
Lawbook is speclaw's answer to the biggest risk with AI agents: **code that
|
|
96
|
+
drifts from intent.** No non-trivial change lands without a spec change — the
|
|
97
|
+
intent is written first, the code is made to match it, and the spec is promoted
|
|
98
|
+
to the project's canonical record. It's a loop of five steps:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
explore → draft → build → sync → archive
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Step | What happens |
|
|
105
|
+
| :-- | :-- |
|
|
106
|
+
| **explore** | Think an idea through *before* committing to it — should we do this, and how. Writes nothing. |
|
|
107
|
+
| **draft** | Create the change under `lawbook/changes/<name>/`: a `proposal.md` (why · what · non-goals), **delta specs** in `specs/<capability>/spec.md`, an optional `design.md`, and a `tasks.md` checklist. |
|
|
108
|
+
| **build** | Implement the tasks in order, keeping code and spec in agreement. |
|
|
109
|
+
| **sync** | Promote the change's delta specs into the canonical `lawbook/specs/` — the always-true description of how the system behaves. |
|
|
110
|
+
| **archive** | Validate, promote, and move the change to `lawbook/changes/archive/` — **in the same PR**, never a post-merge chore. |
|
|
111
|
+
|
|
112
|
+
**Delta specs are normative and testable.** Requirements use `SHALL`/`MUST`
|
|
113
|
+
under `### Requirement:` headers, each with one or more `#### Scenario:` blocks
|
|
114
|
+
whose acceptance criteria hold without production integrations. `lawbook_validate`
|
|
115
|
+
checks that the code matches what the spec promises before you sync or archive.
|
|
116
|
+
|
|
117
|
+
**Three ways to drive it — same engine, no external CLI:**
|
|
118
|
+
|
|
119
|
+
- **In your agent** — the `/lawbook:explore`, `/lawbook:draft`, `/lawbook:build`, `/lawbook:sync`, `/lawbook:archive` commands (installed as skills).
|
|
120
|
+
- **MCP tools** — `lawbook_init`, `lawbook_validate`, `lawbook_sync`, `lawbook_archive`, `lawbook_list`.
|
|
121
|
+
- **CLI** — `speclaw lawbook init | list | validate | sync | archive`.
|
|
122
|
+
|
|
123
|
+
The workspace is committed under `lawbook/`: `specs/` (canonical), `changes/`
|
|
124
|
+
(in-flight), `changes/archive/` (shipped), and `config.yaml` (the mandatory task
|
|
125
|
+
steps every change must include). The standards themselves are amended the same
|
|
126
|
+
way — through a spec change reviewed by a human.
|
|
127
|
+
|
|
128
|
+
<br/>
|
|
129
|
+
|
|
93
130
|
## <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/diamond.png" height="20" alt="◆" align="absmiddle"> Two ways to use it
|
|
94
131
|
|
|
95
132
|
speclaw meets you where you are. Everything works through the **CLI** — so no one
|
|
@@ -104,10 +104,14 @@ export async function runInit(flags) {
|
|
|
104
104
|
ui.step("Indexing your code with Compass");
|
|
105
105
|
const stats = await buildIndex(cwd, (e) => renderProgress(e.done, e.total, e.file));
|
|
106
106
|
clearProgress();
|
|
107
|
-
ui.ok(c.bold(c.cream(String(stats.files))) +
|
|
108
|
-
c.
|
|
109
|
-
c.bold(c.cream(String(stats.
|
|
110
|
-
c.
|
|
107
|
+
ui.ok(c.bold(c.cream(String(stats.files))) +
|
|
108
|
+
c.muted(" files · ") +
|
|
109
|
+
c.bold(c.cream(String(stats.nodes))) +
|
|
110
|
+
c.muted(" nodes · ") +
|
|
111
|
+
c.bold(c.cream(String(stats.edges))) +
|
|
112
|
+
c.muted(" edges · ") +
|
|
113
|
+
c.bold(c.cream(String(stats.embeddings))) +
|
|
114
|
+
c.muted(" embeddings"));
|
|
111
115
|
}
|
|
112
116
|
// 3. Handoff prompt for the chosen agent — printed as a single flush-left
|
|
113
117
|
// line so it copy-pastes cleanly (no borders, no wrapping artifacts).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { specInit, specValidate, specSync, specArchive, specList } from "../../modules/lawbook/engine.js";
|
|
1
|
+
import { specInit, specValidate, specSync, specArchive, specList, } from "../../modules/lawbook/engine.js";
|
|
2
2
|
import { ui } from "../lib/ui.js";
|
|
3
3
|
function today() {
|
|
4
4
|
// The MCP path passes the date in; the CLL runs on a real machine, so read it here.
|
|
@@ -17,7 +17,9 @@ export async function runSpec(flags) {
|
|
|
17
17
|
switch (sub) {
|
|
18
18
|
case "init": {
|
|
19
19
|
const r = specInit(cwd);
|
|
20
|
-
ui.ok(r.alreadyExisted
|
|
20
|
+
ui.ok(r.alreadyExisted
|
|
21
|
+
? "lawbook/ already present"
|
|
22
|
+
: `lawbook/ created (${r.created.length} entries)`);
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
23
25
|
case "list": {
|
|
@@ -39,7 +39,10 @@ export async function runUpdate(flags) {
|
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
ui.step(`Updating ${pkgName()} globally`);
|
|
42
|
-
const install = spawnSync("npm", ["install", "-g", `${pkgName()}@latest`], {
|
|
42
|
+
const install = spawnSync("npm", ["install", "-g", `${pkgName()}@latest`], {
|
|
43
|
+
stdio: "inherit",
|
|
44
|
+
shell: winShell,
|
|
45
|
+
});
|
|
43
46
|
if (install.status !== 0) {
|
|
44
47
|
ui.err("Global update failed. Try again with elevated permissions (e.g. sudo), or check your npm setup.");
|
|
45
48
|
process.exit(1);
|
|
@@ -47,7 +50,10 @@ export async function runUpdate(flags) {
|
|
|
47
50
|
ui.ok(`Updated to ${latest}`);
|
|
48
51
|
// Re-exec the NEWLY installed binary so migrations run with the new assets
|
|
49
52
|
// and any new feature steps — not this (now-stale) process.
|
|
50
|
-
const re = spawnSync("speclaw", ["update", "--migrate-only"], {
|
|
53
|
+
const re = spawnSync("speclaw", ["update", "--migrate-only"], {
|
|
54
|
+
stdio: "inherit",
|
|
55
|
+
shell: winShell,
|
|
56
|
+
});
|
|
51
57
|
if (re.error) {
|
|
52
58
|
ui.warn(`Upgraded — now run ${ui.code("speclaw update --migrate-only")} to apply project changes.`);
|
|
53
59
|
return;
|
|
@@ -5,7 +5,11 @@ import { ui, c } from "../lib/ui.js";
|
|
|
5
5
|
function openInBrowser(file) {
|
|
6
6
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
7
7
|
try {
|
|
8
|
-
spawn(cmd, [file], {
|
|
8
|
+
spawn(cmd, [file], {
|
|
9
|
+
stdio: "ignore",
|
|
10
|
+
detached: true,
|
|
11
|
+
shell: process.platform === "win32",
|
|
12
|
+
}).unref();
|
|
9
13
|
}
|
|
10
14
|
catch {
|
|
11
15
|
/* best-effort */
|
|
@@ -28,8 +32,10 @@ export async function runVisualize(flags) {
|
|
|
28
32
|
limit: flags.limit ? Number(flags.limit) : undefined,
|
|
29
33
|
});
|
|
30
34
|
ui.step("Compass graph");
|
|
31
|
-
ui.ok(c.bold(c.cream(String(r.shown))) +
|
|
32
|
-
c.
|
|
35
|
+
ui.ok(c.bold(c.cream(String(r.shown))) +
|
|
36
|
+
c.muted(" nodes · ") +
|
|
37
|
+
c.bold(c.cream(String(r.links))) +
|
|
38
|
+
c.muted(" edges") +
|
|
33
39
|
(focus ? c.muted(` · focused on ${focus}`) : c.muted(` · top of ${r.total}`)));
|
|
34
40
|
ui.info(`→ ${ui.code(".speclaw/graph.html")}`);
|
|
35
41
|
if (!flags["no-open"]) {
|
package/dist/cli/lib/args.js
CHANGED
|
@@ -39,6 +39,9 @@ export function list(value) {
|
|
|
39
39
|
if (Array.isArray(value))
|
|
40
40
|
return value;
|
|
41
41
|
if (typeof value === "string")
|
|
42
|
-
return value
|
|
42
|
+
return value
|
|
43
|
+
.split(",")
|
|
44
|
+
.map((s) => s.trim())
|
|
45
|
+
.filter(Boolean);
|
|
43
46
|
return [];
|
|
44
47
|
}
|
package/dist/cli/lib/ui.js
CHANGED
|
@@ -11,8 +11,7 @@ const PALETTE = {
|
|
|
11
11
|
amber: [227, 179, 65], // #E3B341 — warning
|
|
12
12
|
red: [235, 90, 90],
|
|
13
13
|
};
|
|
14
|
-
const colorOn = (Boolean(process.stdout.isTTY) || process.env.FORCE_COLOR === "1") &&
|
|
15
|
-
!process.env.NO_COLOR;
|
|
14
|
+
const colorOn = (Boolean(process.stdout.isTTY) || process.env.FORCE_COLOR === "1") && !process.env.NO_COLOR;
|
|
16
15
|
function paint(rgb, s) {
|
|
17
16
|
if (!colorOn)
|
|
18
17
|
return s;
|
|
@@ -56,7 +56,10 @@ async function fetchLatest(name) {
|
|
|
56
56
|
* @returns True when `latest` is strictly newer than `current`.
|
|
57
57
|
*/
|
|
58
58
|
export function isNewer(latest, current) {
|
|
59
|
-
const parse = (v) => v
|
|
59
|
+
const parse = (v) => v
|
|
60
|
+
.split("-")[0]
|
|
61
|
+
.split(".")
|
|
62
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
60
63
|
const a = parse(latest);
|
|
61
64
|
const b = parse(current);
|
|
62
65
|
for (let i = 0; i < 3; i++) {
|
|
@@ -79,7 +82,7 @@ export function isNewer(latest, current) {
|
|
|
79
82
|
export async function checkForUpdates(opts = {}) {
|
|
80
83
|
const current = pkgVersion();
|
|
81
84
|
const cache = readCache();
|
|
82
|
-
let latest
|
|
85
|
+
let latest;
|
|
83
86
|
if (!opts.force && cache && Date.now() - cache.checkedAt < TTL_MS) {
|
|
84
87
|
latest = cache.latest;
|
|
85
88
|
}
|
|
@@ -113,8 +116,17 @@ export async function maybeNotifyUpdate(cmd) {
|
|
|
113
116
|
if (!updateAvailable || !latest)
|
|
114
117
|
return;
|
|
115
118
|
process.stderr.write("\n" +
|
|
116
|
-
" " +
|
|
117
|
-
|
|
119
|
+
" " +
|
|
120
|
+
c.amber("⬆ speclaw ") +
|
|
121
|
+
c.muted(current + " → ") +
|
|
122
|
+
c.cyan(latest) +
|
|
123
|
+
c.muted(" available") +
|
|
124
|
+
"\n" +
|
|
125
|
+
" " +
|
|
126
|
+
c.muted("run ") +
|
|
127
|
+
c.cyan("speclaw update") +
|
|
128
|
+
c.muted(" — upgrades and applies only what's new") +
|
|
129
|
+
"\n\n");
|
|
118
130
|
}
|
|
119
131
|
catch {
|
|
120
132
|
/* the notifier is best-effort — never let it break a command */
|
|
@@ -108,9 +108,7 @@ export function openDb(projectPath) {
|
|
|
108
108
|
if (isStale(db))
|
|
109
109
|
resetSchema(db);
|
|
110
110
|
db.exec(SCHEMA);
|
|
111
|
-
const row = db
|
|
112
|
-
.prepare("SELECT value FROM meta WHERE key = 'schema_version'")
|
|
113
|
-
.get();
|
|
111
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
114
112
|
if (!row) {
|
|
115
113
|
db.prepare("INSERT INTO meta(key, value) VALUES ('schema_version', ?)").run(SCHEMA_VERSION);
|
|
116
114
|
}
|
|
@@ -6,9 +6,23 @@ import { langForPath } from "./languages.js";
|
|
|
6
6
|
import { extract } from "./extract.js";
|
|
7
7
|
import { getEmbedder, toBlob } from "./embedder.js";
|
|
8
8
|
const SKIP_DIRS = new Set([
|
|
9
|
-
".git",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
".git",
|
|
10
|
+
"node_modules",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
".next",
|
|
14
|
+
"out",
|
|
15
|
+
"coverage",
|
|
16
|
+
"__pycache__",
|
|
17
|
+
".venv",
|
|
18
|
+
"venv",
|
|
19
|
+
".speclaw",
|
|
20
|
+
".mypy_cache",
|
|
21
|
+
".pytest_cache",
|
|
22
|
+
"vendor",
|
|
23
|
+
"target",
|
|
24
|
+
".turbo",
|
|
25
|
+
".cache",
|
|
12
26
|
]);
|
|
13
27
|
const MAX_FILE_BYTES = 1_500_000;
|
|
14
28
|
function hashOf(content) {
|
|
@@ -57,8 +71,13 @@ export async function buildIndex(projectPath, onProgress) {
|
|
|
57
71
|
const db = openDb(projectPath);
|
|
58
72
|
const embedder = getEmbedder();
|
|
59
73
|
const stats = {
|
|
60
|
-
files: 0,
|
|
61
|
-
|
|
74
|
+
files: 0,
|
|
75
|
+
nodes: 0,
|
|
76
|
+
edges: 0,
|
|
77
|
+
embeddings: 0,
|
|
78
|
+
unchanged: 0,
|
|
79
|
+
removed: 0,
|
|
80
|
+
embedder: embedder.id,
|
|
62
81
|
};
|
|
63
82
|
const existing = new Map();
|
|
64
83
|
for (const row of db.prepare("SELECT id, path, hash FROM files").all()) {
|
|
@@ -112,7 +112,11 @@ export function explore(projectPath, query) {
|
|
|
112
112
|
callers,
|
|
113
113
|
otherMatches: matches.length > 1
|
|
114
114
|
? matches.slice(1).map((m) => ({
|
|
115
|
-
name: m.name,
|
|
115
|
+
name: m.name,
|
|
116
|
+
kind: m.kind,
|
|
117
|
+
file: m.file,
|
|
118
|
+
line: m.start_line,
|
|
119
|
+
signature: m.signature,
|
|
116
120
|
}))
|
|
117
121
|
: undefined,
|
|
118
122
|
};
|
|
@@ -74,8 +74,10 @@ export function registerCompass(server) {
|
|
|
74
74
|
action: z.enum(["start", "stop", "status"]).describe("start, stop, or status"),
|
|
75
75
|
},
|
|
76
76
|
}, async ({ projectPath, action }) => {
|
|
77
|
-
const result = action === "start"
|
|
78
|
-
|
|
77
|
+
const result = action === "start"
|
|
78
|
+
? startWatch(projectPath)
|
|
79
|
+
: action === "stop"
|
|
80
|
+
? stopWatch(projectPath)
|
|
79
81
|
: watchStatus(projectPath);
|
|
80
82
|
return text(result);
|
|
81
83
|
});
|
|
@@ -3,8 +3,17 @@ import path from "node:path";
|
|
|
3
3
|
import { buildIndex } from "./indexer.js";
|
|
4
4
|
const active = new Map();
|
|
5
5
|
const SKIP = new Set([
|
|
6
|
-
".git",
|
|
7
|
-
"
|
|
6
|
+
".git",
|
|
7
|
+
"node_modules",
|
|
8
|
+
"dist",
|
|
9
|
+
"build",
|
|
10
|
+
".next",
|
|
11
|
+
".speclaw",
|
|
12
|
+
"__pycache__",
|
|
13
|
+
".venv",
|
|
14
|
+
"venv",
|
|
15
|
+
".mypy_cache",
|
|
16
|
+
".pytest_cache",
|
|
8
17
|
]);
|
|
9
18
|
function scheduleReindex(projectPath, state) {
|
|
10
19
|
if (state.timer)
|
|
@@ -15,9 +15,7 @@ export function doctor(projectPath) {
|
|
|
15
15
|
checks.push({
|
|
16
16
|
name: "ai-specs directory",
|
|
17
17
|
ok: has("ai-specs"),
|
|
18
|
-
detail: has("ai-specs")
|
|
19
|
-
? "present"
|
|
20
|
-
: "missing — run the scaffold tool first",
|
|
18
|
+
detail: has("ai-specs") ? "present" : "missing — run the scaffold tool first",
|
|
21
19
|
});
|
|
22
20
|
checks.push({
|
|
23
21
|
name: "LAWS.md constitution",
|
|
@@ -87,9 +85,7 @@ export function doctor(projectPath) {
|
|
|
87
85
|
checks.push({
|
|
88
86
|
name: "lawbook workflow",
|
|
89
87
|
ok: has("lawbook"),
|
|
90
|
-
detail: has("lawbook")
|
|
91
|
-
? "lawbook/ present"
|
|
92
|
-
: "missing — run the `lawbook_init` tool",
|
|
88
|
+
detail: has("lawbook") ? "lawbook/ present" : "missing — run the `lawbook_init` tool",
|
|
93
89
|
});
|
|
94
90
|
checks.push({
|
|
95
91
|
name: "Compass index",
|
|
@@ -7,30 +7,74 @@ import { AGENTS, configureAgent } from "../../shared/agents.js";
|
|
|
7
7
|
import { emptyReport } from "../../shared/install.js";
|
|
8
8
|
const profileShape = {
|
|
9
9
|
project_name: z.string().describe("Short project name, e.g. the repo name"),
|
|
10
|
-
project_description: z
|
|
10
|
+
project_description: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("One-line description of what the project does"),
|
|
11
14
|
organization: z.string().optional().describe("Company/team name"),
|
|
12
|
-
stack_summary: z
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
stack_summary: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("e.g. 'Next.js 15 + TypeScript frontend, FastAPI + PostgreSQL backend'"),
|
|
19
|
+
architecture: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("e.g. 'hexagonal architecture with bounded contexts'"),
|
|
23
|
+
test_commands: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Real commands, e.g. 'pytest backend/tests && npm run test'"),
|
|
27
|
+
lint_commands: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Real commands, e.g. 'ruff check . && npm run lint && tsc --noEmit'"),
|
|
16
31
|
branch_pattern: z.string().optional().describe("e.g. 'feature/<ticket-id>-<slug>'"),
|
|
17
32
|
commit_style: z.string().optional().describe("e.g. 'conventional commits, imperative, English'"),
|
|
18
|
-
custom_laws: z
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
custom_laws: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Extra markdown appended to LAWS.md — project-specific binding rules the analysis surfaced"),
|
|
37
|
+
compass_hints: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Markdown bullets with the repo's real entrypoints and common traces, inserted into docs/compass.md"),
|
|
41
|
+
base_standards_extra: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Markdown with any project-specific cross-cutting rules, appended to docs/standards/base-standards.md"),
|
|
45
|
+
modules_table: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Markdown table of the repo's real modules/bounded contexts + one-line responsibility, for docs/standards/architecture.md"),
|
|
49
|
+
layering_rules: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Markdown describing the layers and their allowed dependencies, for docs/standards/architecture.md"),
|
|
53
|
+
backend_layers: z
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Markdown layer table (Layer | File | Responsibility) from the real backend, for docs/standards/backend-standards.md"),
|
|
57
|
+
frontend_layers: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Markdown layer table from the real frontend, for docs/standards/frontend-standards.md"),
|
|
61
|
+
versioning_rules: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("The repo's versioning/release convention, for docs/standards/conventions.md"),
|
|
65
|
+
documentation_extra: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Repo-specific docstring notes (keep only the languages used, the enforced linter), appended to docs/standards/documentation.md"),
|
|
27
69
|
};
|
|
28
70
|
// ─── The foundation module: analyze the repo, then write the constitution ───
|
|
29
71
|
/** Register the foundation MCP tools (init_project, scaffold, configure_agent, doctor). */
|
|
30
72
|
export function registerFoundation(server) {
|
|
31
73
|
server.registerTool("init_project", {
|
|
32
74
|
description: "START HERE to initialize speclaw in a project. Returns the analysis questionnaire the agent must answer by reading the target repo, plus the available skill packs. Do NOT guess answers — investigate the codebase (package.json, pyproject.toml, CI config, existing docs) and confirm the pack selection with the user before calling scaffold.",
|
|
33
|
-
inputSchema: {
|
|
75
|
+
inputSchema: {
|
|
76
|
+
projectPath: z.string().describe("Absolute path to the project to initialize"),
|
|
77
|
+
},
|
|
34
78
|
}, async () => {
|
|
35
79
|
const packs = loadPacks();
|
|
36
80
|
return text({
|
|
@@ -53,14 +97,19 @@ export function registerFoundation(server) {
|
|
|
53
97
|
projectPath: z.string().describe("Absolute path to the project"),
|
|
54
98
|
profile: z.object(profileShape).describe("Project profile gathered by analyzing the repo"),
|
|
55
99
|
packs: z.array(z.string()).describe("Optional tool pack names (quality, workflow, agents)"),
|
|
56
|
-
agents: z
|
|
100
|
+
agents: z
|
|
101
|
+
.array(z.string())
|
|
102
|
+
.optional()
|
|
103
|
+
.describe(`Agent ids to configure (symlinks + MCP): ${AGENTS.map((a) => a.id).join(", ")}. Usually the CLI handles this; omit to write content only.`),
|
|
57
104
|
},
|
|
58
105
|
}, async ({ projectPath, profile, packs, agents }) => text(scaffold(projectPath, profile, packs, agents ?? [])));
|
|
59
106
|
server.registerTool("configure_agent", {
|
|
60
107
|
description: "Configure one agent's integration in an already-scaffolded project: create its IDE symlinks into ai-specs and register the speclaw MCP server in its config. Re-runnable; add agents one at a time.",
|
|
61
108
|
inputSchema: {
|
|
62
109
|
projectPath: z.string().describe("Absolute path to the project"),
|
|
63
|
-
agent: z
|
|
110
|
+
agent: z
|
|
111
|
+
.enum(AGENTS.map((a) => a.id))
|
|
112
|
+
.describe("Agent id to configure"),
|
|
64
113
|
},
|
|
65
114
|
}, async ({ projectPath, agent }) => {
|
|
66
115
|
const report = emptyReport();
|
|
@@ -115,7 +115,12 @@ export function specValidate(projectPath, change) {
|
|
|
115
115
|
const changeDir = path.join(specRoot(projectPath), "changes", change);
|
|
116
116
|
const issues = [];
|
|
117
117
|
if (!fs.existsSync(changeDir)) {
|
|
118
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
change,
|
|
120
|
+
valid: false,
|
|
121
|
+
issues: [`change "${change}" not found under lawbook/changes/`],
|
|
122
|
+
deltaSpecs: [],
|
|
123
|
+
};
|
|
119
124
|
}
|
|
120
125
|
if (!fs.existsSync(path.join(changeDir, "proposal.md")))
|
|
121
126
|
issues.push("missing proposal.md");
|
|
@@ -138,7 +143,12 @@ export function specValidate(projectPath, change) {
|
|
|
138
143
|
issues.push(`${rel}: no "### Requirement:" header`);
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
|
-
return {
|
|
146
|
+
return {
|
|
147
|
+
change,
|
|
148
|
+
valid: issues.length === 0,
|
|
149
|
+
issues,
|
|
150
|
+
deltaSpecs: deltas.map((f) => path.relative(projectPath, f)),
|
|
151
|
+
};
|
|
142
152
|
}
|
|
143
153
|
/**
|
|
144
154
|
* Promote a change's delta specs into the canonical specs/, overwriting the
|
|
@@ -214,7 +224,8 @@ export function specList(projectPath) {
|
|
|
214
224
|
const abs = path.join(root, rel);
|
|
215
225
|
if (!fs.existsSync(abs))
|
|
216
226
|
return [];
|
|
217
|
-
return fs
|
|
227
|
+
return fs
|
|
228
|
+
.readdirSync(abs, { withFileTypes: true })
|
|
218
229
|
.filter((e) => e.isDirectory() && e.name !== "archive")
|
|
219
230
|
.map((e) => e.name);
|
|
220
231
|
};
|
|
@@ -47,7 +47,10 @@ export function registerSpec(server) {
|
|
|
47
47
|
inputSchema: {
|
|
48
48
|
projectPath: z.string().describe("Absolute path to the project"),
|
|
49
49
|
change: z.string().describe("Change name (folder under lawbook/changes/)"),
|
|
50
|
-
date: z
|
|
50
|
+
date: z
|
|
51
|
+
.string()
|
|
52
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
53
|
+
.describe("Today's date, YYYY-MM-DD"),
|
|
51
54
|
},
|
|
52
55
|
}, async ({ projectPath, change, date }) => text(specArchive(projectPath, change, date)));
|
|
53
56
|
}
|
package/dist/shared/agents.js
CHANGED
|
@@ -3,11 +3,40 @@ import path from "node:path";
|
|
|
3
3
|
import { ensureGitignore } from "./install.js";
|
|
4
4
|
/** The agents speclaw can configure. Add one here and the CLI picks it up. */
|
|
5
5
|
export const AGENTS = [
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
{
|
|
7
|
+
id: "claude",
|
|
8
|
+
label: "Claude Code",
|
|
9
|
+
ideDir: ".claude",
|
|
10
|
+
linkTargets: ["skills", "commands", "agents"],
|
|
11
|
+
mcpFile: ".mcp.json",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "cursor",
|
|
15
|
+
label: "Cursor",
|
|
16
|
+
ideDir: ".cursor",
|
|
17
|
+
linkTargets: ["skills", "commands", "rules"],
|
|
18
|
+
mcpFile: ".cursor/mcp.json",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "codex",
|
|
22
|
+
label: "Codex",
|
|
23
|
+
ideDir: ".codex",
|
|
24
|
+
linkTargets: ["skills", "commands"],
|
|
25
|
+
mcpFile: ".codex/mcp.json",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "windsurf",
|
|
29
|
+
label: "Windsurf",
|
|
30
|
+
ideDir: ".windsurf",
|
|
31
|
+
linkTargets: ["skills", "commands"],
|
|
32
|
+
mcpFile: ".windsurf/mcp.json",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "agents",
|
|
36
|
+
label: "Generic (AGENTS.md)",
|
|
37
|
+
ideDir: ".agents",
|
|
38
|
+
linkTargets: ["skills", "agents"],
|
|
39
|
+
},
|
|
11
40
|
];
|
|
12
41
|
/**
|
|
13
42
|
* Look up a known agent definition by its id.
|
package/dist/shared/manifest.js
CHANGED
|
@@ -12,7 +12,10 @@ function manifestPath(projectPath) {
|
|
|
12
12
|
export function readManifest(projectPath) {
|
|
13
13
|
try {
|
|
14
14
|
const m = JSON.parse(fs.readFileSync(manifestPath(projectPath), "utf8"));
|
|
15
|
-
return {
|
|
15
|
+
return {
|
|
16
|
+
version: String(m.version ?? "0.0.0"),
|
|
17
|
+
packs: Array.isArray(m.packs) ? m.packs.map(String) : [],
|
|
18
|
+
};
|
|
16
19
|
}
|
|
17
20
|
catch {
|
|
18
21
|
return null;
|
package/dist/shared/version.js
CHANGED
|
@@ -10,7 +10,10 @@ function readPkg() {
|
|
|
10
10
|
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
11
11
|
try {
|
|
12
12
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
13
|
-
cached = {
|
|
13
|
+
cached = {
|
|
14
|
+
name: String(pkg.name ?? "@esneiderbravo/speclaw"),
|
|
15
|
+
version: String(pkg.version ?? "0.0.0"),
|
|
16
|
+
};
|
|
14
17
|
}
|
|
15
18
|
catch {
|
|
16
19
|
cached = { name: "@esneiderbravo/speclaw", version: "0.0.0" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esneiderbravo/speclaw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"build": "tsc && node scripts/copy-assets.mjs",
|
|
39
39
|
"brand": "node scripts/render-brand.mjs",
|
|
40
40
|
"start": "node dist/cli/index.js",
|
|
41
|
+
"lint": "eslint .",
|
|
42
|
+
"format": "prettier --write .",
|
|
43
|
+
"check": "prettier --check . && eslint .",
|
|
41
44
|
"prepublishOnly": "npm run build"
|
|
42
45
|
},
|
|
43
46
|
"engines": {
|
|
@@ -52,8 +55,14 @@
|
|
|
52
55
|
"zod": "^3.23.0"
|
|
53
56
|
},
|
|
54
57
|
"devDependencies": {
|
|
58
|
+
"@eslint/js": "^10.0.1",
|
|
55
59
|
"@resvg/resvg-js": "^2.6.2",
|
|
56
60
|
"@types/node": "^22.0.0",
|
|
57
|
-
"
|
|
61
|
+
"eslint": "^10.8.0",
|
|
62
|
+
"eslint-config-prettier": "^10.1.8",
|
|
63
|
+
"globals": "^17.9.0",
|
|
64
|
+
"prettier": "^3.9.6",
|
|
65
|
+
"typescript": "^5.6.0",
|
|
66
|
+
"typescript-eslint": "^8.65.0"
|
|
58
67
|
}
|
|
59
68
|
}
|