@esneiderbravo/speclaw 2.0.13 → 2.0.14
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 +3 -2
- package/dist/cli/lib/help.js +1 -1
- 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 +7 -1
- package/package.json +1 -1
- package/dist/modules/foundation/assets/workflows/speclaw.yml +0 -26
|
@@ -56,7 +56,7 @@ export async function runInit(flags) {
|
|
|
56
56
|
agents: () => clack.multiselect({
|
|
57
57
|
message: "Which agents do you use? (space to select)",
|
|
58
58
|
options: AGENTS.map((a) => ({ value: a.id, label: a.label })),
|
|
59
|
-
initialValues: ["
|
|
59
|
+
initialValues: ["agents"],
|
|
60
60
|
required: true,
|
|
61
61
|
}),
|
|
62
62
|
};
|
|
@@ -77,7 +77,8 @@ export async function runInit(flags) {
|
|
|
77
77
|
packs = answers.packs ?? [];
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
80
|
-
|
|
80
|
+
// Default: one agent-facing folder, `.agents/`, linked into ai-specs/.
|
|
81
|
+
agents = list(flags.agents).length ? list(flags.agents) : ["agents"];
|
|
81
82
|
packs = list(flags.packs);
|
|
82
83
|
}
|
|
83
84
|
const unknownAgents = agents.filter((a) => !agentById(a));
|
package/dist/cli/lib/help.js
CHANGED
|
@@ -129,7 +129,7 @@ npx @esneiderbravo/speclaw@latest init so the scaffold matches the latest releas
|
|
|
129
129
|
|
|
130
130
|
Options
|
|
131
131
|
--yes, -y Accept the defaults without prompting
|
|
132
|
-
--agents <ids>
|
|
132
|
+
--agents <ids> Agents to configure (default: agents → only .agents/; also claude, cursor, codex, windsurf)
|
|
133
133
|
--packs <ids> Comma-separated tool packs to install
|
|
134
134
|
--project-name <name> Project name for the templates
|
|
135
135
|
--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
|
@@ -37,7 +37,7 @@ export const AGENTS = [
|
|
|
37
37
|
id: "agents",
|
|
38
38
|
label: "Generic (AGENTS.md)",
|
|
39
39
|
ideDir: ".agents",
|
|
40
|
-
linkTargets: ["skills", "agents"],
|
|
40
|
+
linkTargets: ["skills", "commands", "agents", "rules"],
|
|
41
41
|
},
|
|
42
42
|
];
|
|
43
43
|
/**
|
|
@@ -141,6 +141,12 @@ export function configureAgent(projectPath, agentId, report) {
|
|
|
141
141
|
if (!agent)
|
|
142
142
|
throw new Error(`Unknown agent "${agentId}". Known: ${AGENTS.map((a) => a.id).join(", ")}`);
|
|
143
143
|
const ideRoot = path.join(projectPath, agent.ideDir);
|
|
144
|
+
// An agent folder speclaw creates holds only links into the gitignored
|
|
145
|
+
// ai-specs/ and per-developer wiring: keep it out of version control. A
|
|
146
|
+
// folder that already existed is the user's and is left as it is.
|
|
147
|
+
if (!fs.existsSync(ideRoot)) {
|
|
148
|
+
ensureGitignore(projectPath, `${agent.ideDir}/`, `speclaw ${agent.label} links (regenerated by init; never commit)`, report);
|
|
149
|
+
}
|
|
144
150
|
for (const target of agent.linkTargets) {
|
|
145
151
|
const sourceDir = path.join(projectPath, "ai-specs", target);
|
|
146
152
|
if (!fs.existsSync(sourceDir))
|
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
|