@esneiderbravo/speclaw 2.0.13 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/init.js +23 -33
- package/dist/cli/lib/help.js +5 -4
- package/dist/modules/foundation/assets/AGENTS.template.md +2 -2
- package/dist/modules/foundation/assets/CLAUDE.template.md +3 -2
- package/dist/modules/foundation/dialects/copilot.js +3 -5
- package/dist/modules/foundation/scaffold.js +3 -16
- package/dist/modules/lawbook/anchors.js +38 -1
- package/dist/shared/agents.js +35 -4
- package/package.json +1 -1
- package/dist/modules/foundation/assets/workflows/speclaw.yml +0 -26
|
@@ -24,17 +24,16 @@ export function detectProjectName(cwd) {
|
|
|
24
24
|
return path.basename(cwd);
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* Setup: create `.agents/` (plus any `--agents`), pick packs when any exist, scaffold, index,
|
|
28
28
|
* and print the handoff prompt.
|
|
29
29
|
*
|
|
30
|
-
* @param flags - Parsed flags;
|
|
31
|
-
* `-y` is set. Honors `--project-name`, `--packs`, and `--no-index`.
|
|
30
|
+
* @param flags - Parsed flags; only the pack picker is interactive (on a TTY unless
|
|
31
|
+
* `--agents`, `--yes`, or `-y` is set). Honors `--project-name`, `--packs`, and `--no-index`.
|
|
32
32
|
*/
|
|
33
33
|
export async function runInit(flags) {
|
|
34
34
|
const cwd = process.cwd();
|
|
35
35
|
const projectName = flags["project-name"] || detectProjectName(cwd);
|
|
36
36
|
const interactive = Boolean(process.stdin.isTTY) && !flags.agents && !flags.yes && !flags.y;
|
|
37
|
-
let agents;
|
|
38
37
|
let packs;
|
|
39
38
|
const availablePacks = loadPacks();
|
|
40
39
|
const packIds = Object.keys(availablePacks);
|
|
@@ -50,35 +49,26 @@ export async function runInit(flags) {
|
|
|
50
49
|
`Later, ${ui.code("speclaw update")} upgrades itself through npx.`);
|
|
51
50
|
ui.plain();
|
|
52
51
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
const answers = await clack.group(prompts, { onCancel: () => process.exit(1) });
|
|
76
|
-
agents = answers.agents;
|
|
77
|
-
packs = answers.packs ?? [];
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
agents = list(flags.agents).length ? list(flags.agents) : ["claude"];
|
|
81
|
-
packs = list(flags.packs);
|
|
52
|
+
// Agents are never prompted for: init creates one agent-facing folder,
|
|
53
|
+
// `.agents/`, linked into ai-specs/. Other IDE folders only via `--agents`
|
|
54
|
+
// or `speclaw agent add`.
|
|
55
|
+
const agents = list(flags.agents).length ? list(flags.agents) : ["agents"];
|
|
56
|
+
packs = list(flags.packs);
|
|
57
|
+
// Covers: req~remove-agents-pack~1
|
|
58
|
+
if (interactive && packIds.length > 0 && !packs.length) {
|
|
59
|
+
const answer = await clack.multiselect({
|
|
60
|
+
message: "Which tool packs to install?",
|
|
61
|
+
options: packIds.map((id) => ({
|
|
62
|
+
value: id,
|
|
63
|
+
label: id,
|
|
64
|
+
hint: availablePacks[id].description.slice(0, 50),
|
|
65
|
+
})),
|
|
66
|
+
initialValues: [],
|
|
67
|
+
required: false,
|
|
68
|
+
});
|
|
69
|
+
if (clack.isCancel(answer))
|
|
70
|
+
process.exit(1);
|
|
71
|
+
packs = answer;
|
|
82
72
|
}
|
|
83
73
|
const unknownAgents = agents.filter((a) => !agentById(a));
|
|
84
74
|
if (unknownAgents.length) {
|
package/dist/cli/lib/help.js
CHANGED
|
@@ -13,7 +13,7 @@ Install globally so the command is always available:
|
|
|
13
13
|
npm i -g @esneiderbravo/speclaw (or run npx @esneiderbravo/speclaw@latest init)
|
|
14
14
|
|
|
15
15
|
Setup
|
|
16
|
-
init
|
|
16
|
+
init Setup: .agents/ linked into ai-specs/, scaffold, index, get the prompt
|
|
17
17
|
(--minimal omits setup/lifecycle MCP tools)
|
|
18
18
|
update Re-run itself at the latest version via npx, then apply project
|
|
19
19
|
migrations (--check reports version only; --no-self-update
|
|
@@ -123,13 +123,14 @@ you rarely run it by hand.
|
|
|
123
123
|
name: "init",
|
|
124
124
|
usage: `Usage: speclaw init [options]
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
Compass index, and print the
|
|
126
|
+
Setup without an agent picker: create .agents/ linked into ai-specs/ (gitignored),
|
|
127
|
+
scaffold the constitution and lawbook, build the Compass index, and print the
|
|
128
|
+
starting prompt. Prefer
|
|
128
129
|
npx @esneiderbravo/speclaw@latest init so the scaffold matches the latest release.
|
|
129
130
|
|
|
130
131
|
Options
|
|
131
132
|
--yes, -y Accept the defaults without prompting
|
|
132
|
-
--agents <ids>
|
|
133
|
+
--agents <ids> Agents to configure (default: agents → only .agents/; also claude, cursor, codex, windsurf)
|
|
133
134
|
--packs <ids> Comma-separated tool packs to install
|
|
134
135
|
--project-name <name> Project name for the templates
|
|
135
136
|
--minimal Omit setup/lifecycle MCP tools
|
|
@@ -42,8 +42,8 @@ Claude-specific notes: [`CLAUDE.md`](CLAUDE.md). The law: [`LAWS.md`](LAWS.md).
|
|
|
42
42
|
[`docs/standards/base-standards.md`](docs/standards/base-standards.md) and
|
|
43
43
|
[`docs/standards/conventions.md`](docs/standards/conventions.md).
|
|
44
44
|
6. **Use the skills and role agents.** `ai-specs/` is the canonical home for
|
|
45
|
-
skills, commands, rules, and agents
|
|
46
|
-
|
|
45
|
+
skills, commands, rules, and agents; agents read them through the symlinks
|
|
46
|
+
in `.agents/` (plus an IDE folder only for an agent added explicitly).
|
|
47
47
|
7. **Ask before irreversible or outward-facing actions** — destructive commands;
|
|
48
48
|
writing to a real data store (DB rows or files with real user data, including
|
|
49
49
|
for tests — verify against an isolated/throwaway store); publishing
|
|
@@ -82,8 +82,9 @@ its neighbors, comments that carry constraints (never ticket IDs). Full rules:
|
|
|
82
82
|
|
|
83
83
|
## Rule 5 — Skills and role agents are law-adjacent
|
|
84
84
|
|
|
85
|
-
Skills, commands, rules, and agents live in `ai-specs
|
|
86
|
-
|
|
85
|
+
Skills, commands, rules, and agents live in `ai-specs/`, read through the
|
|
86
|
+
symlinks in `.agents/` (an IDE folder such as `.claude/` exists only for an
|
|
87
|
+
agent added explicitly with `speclaw agent add`). When a skill or role matches
|
|
87
88
|
the task, use it — do not improvise a parallel process.
|
|
88
89
|
|
|
89
90
|
## Rule 6 — Stop conditions
|
|
@@ -6,11 +6,9 @@ import { frontmatter, lawSlug, } from "./types.js";
|
|
|
6
6
|
export const copilotDialect = {
|
|
7
7
|
id: "copilot-instructions",
|
|
8
8
|
compile(laws, ctx) {
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
const want = ctx.agents.includes("copilot") ||
|
|
12
|
-
ctx.agents.includes("github-copilot") ||
|
|
13
|
-
ctx.agents.includes("agents");
|
|
9
|
+
// Only for Copilot: the generic agent reads scoped laws from AGENTS.md
|
|
10
|
+
// (agentsmd dialect), so it gets no `.github/instructions/` folder.
|
|
11
|
+
const want = ctx.agents.includes("copilot") || ctx.agents.includes("github-copilot");
|
|
14
12
|
if (!want)
|
|
15
13
|
return [];
|
|
16
14
|
const scoped = laws.filter((l) => (l.status ?? "active") !== "draft" && l.scope.length > 0);
|
|
@@ -50,21 +50,6 @@ function ensureLawManifest(projectPath, report) {
|
|
|
50
50
|
}
|
|
51
51
|
return manifest;
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Write `.github/workflows/speclaw.yml` from the shipped template when the
|
|
55
|
-
* path does not exist. Never overwrite — the user's CI is theirs.
|
|
56
|
-
*/
|
|
57
|
-
function ensureVerifyWorkflow(projectPath, report) {
|
|
58
|
-
const dest = path.join(projectPath, ".github", "workflows", "speclaw.yml");
|
|
59
|
-
if (fs.existsSync(dest)) {
|
|
60
|
-
report.skipped.push(dest);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const src = path.join(ASSETS, "workflows", "speclaw.yml");
|
|
64
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
65
|
-
fs.copyFileSync(src, dest);
|
|
66
|
-
report.written.push(dest);
|
|
67
|
-
}
|
|
68
53
|
/**
|
|
69
54
|
* Render the foundation: walk the module's assets/, mirror its structure into
|
|
70
55
|
* the project, stripping the `.template` marker (foo.template.md -> foo.md).
|
|
@@ -163,6 +148,9 @@ export function scaffold(projectPath, profile, packNames, agents = [], opts = {}
|
|
|
163
148
|
// it out of the module assets) plus the local .speclaw.json manifest — local,
|
|
164
149
|
// per-checkout content, reconstructed by init/update, never committed.
|
|
165
150
|
ensureGitignore(projectPath, "ai-specs/", "speclaw workflow content (regenerated by init/update; never commit)", report);
|
|
151
|
+
// The integrity lock is local tool state: it guards this checkout's rule
|
|
152
|
+
// files and is rebuilt by init/update, so it is not committed.
|
|
153
|
+
ensureGitignore(projectPath, "speclaw.lock", "speclaw local integrity lock (never commit)", report);
|
|
166
154
|
for (const id of agents)
|
|
167
155
|
configureAgent(projectPath, id, report); // only the chosen agents
|
|
168
156
|
// Compile the declared laws into agent hooks for every hook-capable agent just
|
|
@@ -185,7 +173,6 @@ export function scaffold(projectPath, profile, packNames, agents = [], opts = {}
|
|
|
185
173
|
report.lockError = err.message;
|
|
186
174
|
}
|
|
187
175
|
}
|
|
188
|
-
ensureVerifyWorkflow(projectPath, report);
|
|
189
176
|
report.hooks = installHooks(projectPath, agents, lawManifest, report, {
|
|
190
177
|
baselines: managedOpts.baselines,
|
|
191
178
|
backup: managedOpts.backup,
|
|
@@ -241,7 +241,8 @@ export function sealCapability(projectPath, capability, markdown, opts = {}) {
|
|
|
241
241
|
const now = opts.now ?? new Date().toISOString();
|
|
242
242
|
const sha = headSha(projectPath);
|
|
243
243
|
const specId = opts.specId ?? capability;
|
|
244
|
-
const
|
|
244
|
+
const fresh = resolveCandidates(db, projectPath, extractCandidates(markdown), specId, now, sha);
|
|
245
|
+
const rows = keepUnchangedStamps(readAnchorsFile(projectPath, capability), fresh);
|
|
245
246
|
const dest = writeAnchorsFile(projectPath, {
|
|
246
247
|
anchorsVersion: 1,
|
|
247
248
|
capability,
|
|
@@ -262,6 +263,42 @@ export function sealCapability(projectPath, capability, markdown, opts = {}) {
|
|
|
262
263
|
db.close();
|
|
263
264
|
}
|
|
264
265
|
}
|
|
266
|
+
/** Fields that identify one anchor across seals. */
|
|
267
|
+
function anchorKey(a) {
|
|
268
|
+
return [a.requirementId, a.scenarioId, a.symbolName, a.anchorKind, a.source].join("\0");
|
|
269
|
+
}
|
|
270
|
+
/** Fields whose change means the anchor was really re-sealed. */
|
|
271
|
+
function sealedState(a) {
|
|
272
|
+
return JSON.stringify([
|
|
273
|
+
a.specId,
|
|
274
|
+
a.filePath,
|
|
275
|
+
a.resolution,
|
|
276
|
+
a.contentHash,
|
|
277
|
+
a.rawHash,
|
|
278
|
+
a.normalizerVersion,
|
|
279
|
+
]);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Keep the previous `archivedAt` / `commitSha` of every anchor whose sealed
|
|
283
|
+
* state did not change, so a reseal rewrites only the anchors that moved —
|
|
284
|
+
* an unchanged capability stays byte-identical and the drift age of an
|
|
285
|
+
* untouched anchor still counts from when it was really sealed.
|
|
286
|
+
*
|
|
287
|
+
* @param previous - The capability's anchors file before this seal, if any.
|
|
288
|
+
* @param fresh - The anchors just resolved, stamped with this seal's time and sha.
|
|
289
|
+
* @returns The anchors to write.
|
|
290
|
+
*/
|
|
291
|
+
function keepUnchangedStamps(previous, fresh) {
|
|
292
|
+
if (!previous)
|
|
293
|
+
return fresh;
|
|
294
|
+
const old = new Map(previous.anchors.map((a) => [anchorKey(a), a]));
|
|
295
|
+
return fresh.map((a) => {
|
|
296
|
+
const prior = old.get(anchorKey(a));
|
|
297
|
+
return prior && sealedState(prior) === sealedState(a)
|
|
298
|
+
? { ...a, archivedAt: prior.archivedAt, commitSha: prior.commitSha }
|
|
299
|
+
: a;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
265
302
|
/** Seal every canonical capability under lawbook/specs/ that has a spec.md. */
|
|
266
303
|
export function resealAll(projectPath) {
|
|
267
304
|
const specsRoot = path.join(projectPath, "lawbook", "specs");
|
package/dist/shared/agents.js
CHANGED
|
@@ -11,6 +11,7 @@ export const AGENTS = [
|
|
|
11
11
|
linkTargets: ["skills", "commands", "agents"],
|
|
12
12
|
mcpFile: ".mcp.json",
|
|
13
13
|
hooks: { file: ".claude/settings.json", key: "hooks" },
|
|
14
|
+
ownedPaths: [".claude/rules/speclaw"],
|
|
14
15
|
},
|
|
15
16
|
{
|
|
16
17
|
id: "cursor",
|
|
@@ -37,7 +38,7 @@ export const AGENTS = [
|
|
|
37
38
|
id: "agents",
|
|
38
39
|
label: "Generic (AGENTS.md)",
|
|
39
40
|
ideDir: ".agents",
|
|
40
|
-
linkTargets: ["skills", "agents"],
|
|
41
|
+
linkTargets: ["skills", "commands", "agents", "rules"],
|
|
41
42
|
},
|
|
42
43
|
];
|
|
43
44
|
/**
|
|
@@ -141,6 +142,24 @@ export function configureAgent(projectPath, agentId, report) {
|
|
|
141
142
|
if (!agent)
|
|
142
143
|
throw new Error(`Unknown agent "${agentId}". Known: ${AGENTS.map((a) => a.id).join(", ")}`);
|
|
143
144
|
const ideRoot = path.join(projectPath, agent.ideDir);
|
|
145
|
+
// Everything speclaw puts in an agent folder is links into the gitignored
|
|
146
|
+
// ai-specs/ plus per-developer wiring: keep it out of version control. A
|
|
147
|
+
// folder speclaw creates is ignored whole; in a folder that already existed
|
|
148
|
+
// (the IDE's own, e.g. `.claude/settings.local.json`) only speclaw's entries are.
|
|
149
|
+
const comment = `speclaw ${agent.label} links (regenerated by init; never commit)`;
|
|
150
|
+
if (!fs.existsSync(ideRoot)) {
|
|
151
|
+
ensureGitignore(projectPath, `${agent.ideDir}/`, comment, report);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const owned = agent.linkTargets
|
|
155
|
+
.filter((t) => fs.existsSync(path.join(projectPath, "ai-specs", t)))
|
|
156
|
+
.map((t) => `${agent.ideDir}/${t}`)
|
|
157
|
+
.concat(agent.ownedPaths ?? []);
|
|
158
|
+
if (agent.hooks && !fs.existsSync(path.join(projectPath, agent.hooks.file)))
|
|
159
|
+
owned.push(agent.hooks.file);
|
|
160
|
+
for (const entry of owned)
|
|
161
|
+
ensureGitignore(projectPath, entry, comment, report);
|
|
162
|
+
}
|
|
144
163
|
for (const target of agent.linkTargets) {
|
|
145
164
|
const sourceDir = path.join(projectPath, "ai-specs", target);
|
|
146
165
|
if (!fs.existsSync(sourceDir))
|
|
@@ -158,13 +177,25 @@ export function configureAgent(projectPath, agentId, report) {
|
|
|
158
177
|
writeMcpConfig(projectPath, agent.mcpFile, report);
|
|
159
178
|
}
|
|
160
179
|
/**
|
|
161
|
-
* List the agents
|
|
180
|
+
* List the agents speclaw set up in this project: those whose IDE dir holds at
|
|
181
|
+
* least one speclaw link into ai-specs/. A bare IDE dir is not enough — Claude
|
|
182
|
+
* Code and Cursor create `.claude/` / `.cursor/` on their own, and treating
|
|
183
|
+
* that as "configured" made `update` populate folders nobody chose.
|
|
162
184
|
*
|
|
163
185
|
* @param projectPath - Project root to inspect.
|
|
164
|
-
* @returns The ids of agents
|
|
186
|
+
* @returns The ids of agents with a speclaw link in their IDE directory.
|
|
165
187
|
*/
|
|
166
188
|
export function detectConfiguredAgents(projectPath) {
|
|
167
|
-
return AGENTS.filter((a) =>
|
|
189
|
+
return AGENTS.filter((a) => a.linkTargets.some((t) => isAiSpecsLink(path.join(projectPath, a.ideDir, t), t))).map((a) => a.id);
|
|
190
|
+
}
|
|
191
|
+
/** True when `p` is a symlink whose target ends in `ai-specs/<target>`. */
|
|
192
|
+
function isAiSpecsLink(p, target) {
|
|
193
|
+
try {
|
|
194
|
+
return fs.readlinkSync(p).replace(/\\/g, "/").endsWith(`ai-specs/${target}`);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
168
199
|
}
|
|
169
200
|
/**
|
|
170
201
|
* Re-run configuration for every already-configured agent (e.g. after adding a pack).
|
package/package.json
CHANGED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Consumer template. init/update write this only when the path is missing.
|
|
2
|
-
# Never the untrusted PR trigger. The verify job has no secrets.
|
|
3
|
-
name: ⚖️ speclaw
|
|
4
|
-
on: pull_request
|
|
5
|
-
|
|
6
|
-
permissions: {}
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
verify:
|
|
10
|
-
name: ⚖️ Verify laws
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
permissions:
|
|
13
|
-
contents: read
|
|
14
|
-
security-events: write
|
|
15
|
-
steps:
|
|
16
|
-
- name: 📥 Checkout
|
|
17
|
-
uses: actions/checkout@v5
|
|
18
|
-
with:
|
|
19
|
-
fetch-depth: 0
|
|
20
|
-
- name: ⚖️ Verify
|
|
21
|
-
uses: esneiderbravo/speclaw@v2
|
|
22
|
-
- name: 🛡️ Upload SARIF
|
|
23
|
-
uses: github/codeql-action/upload-sarif@v4
|
|
24
|
-
if: always()
|
|
25
|
-
with:
|
|
26
|
-
sarif_file: speclaw.sarif
|