@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.
@@ -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: ["claude"],
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
- agents = list(flags.agents).length ? list(flags.agents) : ["claude"];
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));
@@ -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> Comma-separated agents to configure (claude, cursor, …)
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, mirrored to each IDE directory via
46
- symlinks.
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/` (symlinked into
86
- `.claude/`, `.cursor/`, `.codex/`, `.agents/`). When a skill or role matches
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
- // Emit when explicitly wanted: agent id "copilot" or always if .github exists —
10
- // orchestrator passes agents; treat missing copilot as skip unless "agents" generic.
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 rows = resolveCandidates(db, projectPath, extractCandidates(markdown), specId, now, sha);
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");
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@esneiderbravo/speclaw",
3
- "version": "2.0.13",
3
+ "version": "2.0.14",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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