jig-ui 0.23.0 → 0.24.0

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/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { readFileSync as readFileSync36 } from "fs";
6
- import { homedir as homedir2 } from "os";
7
- import { join as join43 } from "path";
5
+ import { existsSync as existsSync28, readFileSync as readFileSync37 } from "fs";
6
+ import { homedir as homedir3 } from "os";
7
+ import { join as join44 } from "path";
8
8
 
9
9
  // src/paths.ts
10
10
  import { existsSync } from "fs";
@@ -55,8 +55,8 @@ function cliInvocation(version2) {
55
55
  var JIG_GATE_HOOK = /(?:\bjig-ui@[^\s"]+|^jig) gate\b/;
56
56
 
57
57
  // src/commands/install.ts
58
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
59
- import { join as join7 } from "path";
58
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
59
+ import { join as join8 } from "path";
60
60
 
61
61
  // src/adapters/types.ts
62
62
  var COMMAND_DESCRIPTION = "Run a Jig design-system command: set the project up, plan and build one feature at a time (decide, spec, mockup, make, critique), make a small change to a built page (tweak), check the UI against the rules, or refresh the install.";
@@ -88,6 +88,236 @@ function agentsPointer(skillPath) {
88
88
  ].join("\n");
89
89
  }
90
90
 
91
+ // src/install/agent-files.ts
92
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
93
+ import { homedir } from "os";
94
+ import { join as join4 } from "path";
95
+
96
+ // src/text.ts
97
+ import { readFileSync } from "fs";
98
+ function lf(text) {
99
+ return text.replace(/\r\n/g, "\n");
100
+ }
101
+ function readText(path) {
102
+ return lf(readFileSync(path, "utf8"));
103
+ }
104
+
105
+ // src/install/atomic.ts
106
+ import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
107
+ import { dirname as dirname2, join as join2 } from "path";
108
+ function writeFileAtomic(path, content) {
109
+ const dir = dirname2(path);
110
+ mkdirSync(dir, { recursive: true });
111
+ const tmp = join2(dir, `.${process.pid}-${Date.now()}.tmp`);
112
+ try {
113
+ writeFileSync(tmp, content, "utf8");
114
+ renameWithRetry(tmp, path);
115
+ } catch (err) {
116
+ try {
117
+ unlinkSync(tmp);
118
+ } catch {
119
+ }
120
+ throw err;
121
+ }
122
+ }
123
+ function renameWithRetry(from, to) {
124
+ const retryable = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
125
+ for (let waited = 0, delay = 10; ; waited += delay, delay = Math.min(delay * 2, 200)) {
126
+ try {
127
+ renameSync(from, to);
128
+ return;
129
+ } catch (err) {
130
+ const code = err.code ?? "";
131
+ if (process.platform !== "win32" || !retryable.has(code) || waited > 2e3) throw err;
132
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
133
+ }
134
+ }
135
+ }
136
+
137
+ // src/install/vendor.ts
138
+ import { posix } from "path";
139
+ var { join: join3 } = posix;
140
+ var DELIMITERS = {
141
+ html: ["<!--", "-->"],
142
+ css: ["/*", "*/"]
143
+ };
144
+ function licencePathFor(relPath) {
145
+ const depth = relPath.split("/").length - 1;
146
+ return depth === 0 ? "." : Array(depth).fill("..").join("/");
147
+ }
148
+ function vendorHeader(file, version2, style = "html", licencePath = ".") {
149
+ const [open, close] = DELIMITERS[style];
150
+ const attribution = licencePath === null ? " Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill." : ` Licensed Apache-2.0. See ${join3(licencePath, "LICENSE")} and ${join3(licencePath, "NOTICE")}.`;
151
+ return [
152
+ `${open} ${file} \u2014 vendored from Jig v${version2}.`,
153
+ attribution,
154
+ ` Edit freely: \`jig update\` will not overwrite a file you have changed. ${close}`,
155
+ "",
156
+ ""
157
+ ].join("\n");
158
+ }
159
+ function findMarkerRegions(existing) {
160
+ const regions = [];
161
+ let searchFrom = 0;
162
+ let openStart = null;
163
+ while (true) {
164
+ const nextStart = existing.indexOf(BLOCK_START, searchFrom);
165
+ const nextEnd = existing.indexOf(BLOCK_END, searchFrom);
166
+ if (nextStart === -1 && nextEnd === -1) break;
167
+ const endIsNext = nextEnd !== -1 && (nextStart === -1 || nextEnd < nextStart);
168
+ if (endIsNext) {
169
+ if (openStart !== null) {
170
+ regions.push({ start: openStart, end: nextEnd + BLOCK_END.length });
171
+ openStart = null;
172
+ } else {
173
+ regions.push({ start: nextEnd, end: nextEnd + BLOCK_END.length });
174
+ }
175
+ searchFrom = nextEnd + BLOCK_END.length;
176
+ } else {
177
+ if (openStart !== null) {
178
+ regions.push({ start: openStart, end: openStart + BLOCK_START.length });
179
+ }
180
+ openStart = nextStart;
181
+ searchFrom = nextStart + BLOCK_START.length;
182
+ }
183
+ }
184
+ if (openStart !== null) {
185
+ regions.push({ start: openStart, end: existing.length });
186
+ }
187
+ regions.sort((a, b) => a.start - b.start);
188
+ return regions;
189
+ }
190
+ function stripMarkerRegions(existing) {
191
+ const regions = findMarkerRegions(existing);
192
+ if (regions.length === 0) {
193
+ return { text: existing, insertAt: existing.length, removed: false };
194
+ }
195
+ let text = "";
196
+ let cursor = 0;
197
+ let insertAt = -1;
198
+ regions.forEach((region, i) => {
199
+ let chunk = existing.slice(cursor, region.start);
200
+ if (i > 0) chunk = chunk.replace(/^\n/, "");
201
+ text += chunk;
202
+ if (insertAt === -1) insertAt = text.length;
203
+ cursor = region.end;
204
+ });
205
+ const tail = existing.slice(cursor).replace(/^\n/, "");
206
+ text += tail;
207
+ return { text, insertAt, removed: true };
208
+ }
209
+ function upsertBlock(existing, block) {
210
+ const { text, insertAt, removed } = stripMarkerRegions(existing);
211
+ if (!removed) {
212
+ const separator = text.length && !text.endsWith("\n") ? "\n\n" : text.length ? "\n" : "";
213
+ return `${text}${separator}${block}`;
214
+ }
215
+ return `${text.slice(0, insertAt)}${block}${text.slice(insertAt)}`;
216
+ }
217
+
218
+ // src/install/agent-files.ts
219
+ var AGENTS_FILE = "AGENTS.md";
220
+ var CLAUDE_FILE = "CLAUDE.md";
221
+ function agentsGuide() {
222
+ return [
223
+ BLOCK_START,
224
+ "",
225
+ "# Jig: the design system for this project's UI",
226
+ "",
227
+ "Before building or reviewing any interface, load the Jig skill and follow it:",
228
+ "`.agents/skills/jig/SKILL.md`, or `skills/jig/SKILL.md` in your agent's own",
229
+ "folder (`.claude/skills/jig/SKILL.md` for Claude Code). It says which rule",
230
+ "files to load for the task.",
231
+ "",
232
+ "## How work goes",
233
+ "",
234
+ "- `/jig decide`, once: the project's design decisions, from the owner.",
235
+ "- For each page or feature: `/jig spec`, then `/jig mockup` (or skip it), then",
236
+ " `/jig make`, then `/jig critique` (now, or left for `/jig ship`).",
237
+ "- A small change to a built page: `/jig tweak`.",
238
+ "- Before a release: `/jig ship`.",
239
+ "- After any change to UI files: `jig check`.",
240
+ "",
241
+ 'With no slash commands, ask in plain words: "run jig spec for the pricing page".',
242
+ "",
243
+ "## The owner's word",
244
+ "",
245
+ "A spec confirmed, a drawing approved or skipped, a critique left for later, a",
246
+ "decision's reason: each is recorded in the owner's own words. Never infer one",
247
+ "from something else they said; ask.",
248
+ "",
249
+ "## Jig's files",
250
+ "",
251
+ "- `jig.config.json`: the mode each route uses, files `check` skips (`exempt`),",
252
+ " and when pages are critiqued.",
253
+ "- The token folder: `brand.*.css` is the project's identity, edit it freely;",
254
+ " `mode.*.css` is Jig's, refreshed by `jig update`; `theme.css` imports both.",
255
+ "- `DECISIONS.md`, beside the tokens: the project's decisions, written by",
256
+ " `/jig decide` and `/jig tweak` only.",
257
+ "- `.jig/specs/`: what each page must be; `<name>.checked.json` is its check.",
258
+ "- `.jig/mockups/`: approved drawings.",
259
+ "- `.jig/critique/<page>/`: a critique's verdicts, its report and its lock.",
260
+ "- `.jig/state.json`: what Jig wrote, so `jig update` leaves your edits alone.",
261
+ "",
262
+ "Jig keeps this block current. Write your own instructions above or below it.",
263
+ "",
264
+ BLOCK_END,
265
+ ""
266
+ ].join("\n");
267
+ }
268
+ function claudeImport() {
269
+ return [
270
+ BLOCK_START,
271
+ "",
272
+ "Jig's instructions for this project are in AGENTS.md, imported here:",
273
+ "",
274
+ `@${AGENTS_FILE}`,
275
+ "",
276
+ BLOCK_END,
277
+ ""
278
+ ].join("\n");
279
+ }
280
+ function claudeInstalled(projectRoot, homeDir = homedir()) {
281
+ return existsSync2(join4(projectRoot, ".claude", "skills", "jig", "SKILL.md")) || existsSync2(join4(homeDir, ".claude", "skills", "jig", "SKILL.md"));
282
+ }
283
+ function blockIn(text) {
284
+ const t = lf(text);
285
+ const start = t.indexOf(BLOCK_START);
286
+ const end = t.indexOf(BLOCK_END, start);
287
+ return start >= 0 && end > start ? t.slice(start, end + BLOCK_END.length) : void 0;
288
+ }
289
+ var expected = (block) => block.trim();
290
+ function ensureAgentFiles(projectRoot, opts) {
291
+ const wanted = [[AGENTS_FILE, agentsGuide()]];
292
+ if (opts.claude) wanted.push([CLAUDE_FILE, claudeImport()]);
293
+ const written = [];
294
+ for (const [file, block] of wanted) {
295
+ const path = join4(projectRoot, file);
296
+ const before = existsSync2(path) ? readFileSync2(path, "utf8") : "";
297
+ if (blockIn(before) === expected(block)) continue;
298
+ writeFileAtomic(path, upsertBlock(before, block));
299
+ written.push(file);
300
+ }
301
+ return written;
302
+ }
303
+ function agentFileProblems(projectRoot, homeDir = homedir()) {
304
+ if (!existsSync2(join4(projectRoot, ".jig", "state.json"))) return [];
305
+ const wanted = [[AGENTS_FILE, agentsGuide()]];
306
+ if (claudeInstalled(projectRoot, homeDir)) wanted.push([CLAUDE_FILE, claudeImport()]);
307
+ const problems = [];
308
+ for (const [file, block] of wanted) {
309
+ const path = join4(projectRoot, file);
310
+ if (!existsSync2(path)) {
311
+ problems.push(`${file} is missing, so an agent starts without Jig's instructions.`);
312
+ continue;
313
+ }
314
+ const found = blockIn(readFileSync2(path, "utf8"));
315
+ if (!found) problems.push(`${file} has no Jig block, so an agent starts without Jig's instructions.`);
316
+ else if (found !== expected(block)) problems.push(`${file}: Jig's block has been edited or is out of date.`);
317
+ }
318
+ return problems.map((p) => `${p} Run \`jig update\` to put it back; your own text in the file stays as it is.`);
319
+ }
320
+
91
321
  // src/adapters/codex.ts
92
322
  var codex = {
93
323
  name: "codex",
@@ -96,7 +326,7 @@ var codex = {
96
326
  const skillPath = ".agents/skills/jig/SKILL.md";
97
327
  const files = [{ relPath: skillPath, content: ctx.skillBody }];
98
328
  const agentsPath = ctx.scope === "global" ? ".codex/AGENTS.md" : "AGENTS.md";
99
- files.push({ relPath: agentsPath, content: agentsPointer(skillPath) });
329
+ files.push({ relPath: agentsPath, content: ctx.scope === "global" ? agentsPointer(skillPath) : agentsGuide() });
100
330
  if (ctx.commandBody && ctx.scope === "global") {
101
331
  files.push({ relPath: ".codex/prompts/jig.md", content: `${ctx.commandBody}
102
332
  ` });
@@ -258,22 +488,22 @@ function skillFilesFor(adapter, ctx) {
258
488
  }
259
489
 
260
490
  // src/install/references.ts
261
- import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
262
- import { join as join2, posix } from "path";
491
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
492
+ import { join as join5, posix as posix2 } from "path";
263
493
  function referenceFiles(packageRoot2) {
264
- const root = join2(packageRoot2, "references");
265
- if (!existsSync2(root)) return [];
494
+ const root = join5(packageRoot2, "references");
495
+ if (!existsSync3(root)) return [];
266
496
  const found = [];
267
497
  const walk2 = (dir, prefix) => {
268
498
  for (const entry of readdirSync(dir, { withFileTypes: true }).sort(
269
499
  (a, b) => a.name.localeCompare(b.name)
270
500
  )) {
271
- const child = join2(dir, entry.name);
272
- const rel = prefix ? posix.join(prefix, entry.name) : entry.name;
501
+ const child = join5(dir, entry.name);
502
+ const rel = prefix ? posix2.join(prefix, entry.name) : entry.name;
273
503
  if (entry.isDirectory()) {
274
504
  walk2(child, rel);
275
505
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
276
- found.push({ relPath: rel, content: readFileSync(child, "utf8") });
506
+ found.push({ relPath: rel, content: readFileSync3(child, "utf8") });
277
507
  }
278
508
  }
279
509
  };
@@ -283,32 +513,11 @@ function referenceFiles(packageRoot2) {
283
513
 
284
514
  // src/install/manifest.ts
285
515
  import { createHash } from "crypto";
286
- import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, statSync, unlinkSync as unlinkSync2, writeSync } from "fs";
287
- import { dirname as dirname3, join as join4 } from "path";
288
-
289
- // src/install/atomic.ts
290
- import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
291
- import { dirname as dirname2, join as join3 } from "path";
292
- function writeFileAtomic(path, content) {
293
- const dir = dirname2(path);
294
- mkdirSync(dir, { recursive: true });
295
- const tmp = join3(dir, `.${process.pid}-${Date.now()}.tmp`);
296
- try {
297
- writeFileSync(tmp, content, "utf8");
298
- renameSync(tmp, path);
299
- } catch (err) {
300
- try {
301
- unlinkSync(tmp);
302
- } catch {
303
- }
304
- throw err;
305
- }
306
- }
307
-
308
- // src/install/manifest.ts
516
+ import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync4, statSync, unlinkSync as unlinkSync2, writeSync } from "fs";
517
+ import { dirname as dirname3, join as join6 } from "path";
309
518
  var REGENERATE_HINT = "Re-run 'jig install --agent <name>' to fix it.";
310
519
  function manifestPath(root, manifestDir) {
311
- return join4(root, ...manifestDir.split("/"), "manifest.json");
520
+ return join6(root, ...manifestDir.split("/"), "manifest.json");
312
521
  }
313
522
  function checksum(content) {
314
523
  const normalized = content.replace(/\r\n/g, "\n");
@@ -346,10 +555,10 @@ function validateManifest(raw, path) {
346
555
  }
347
556
  function readManifest(root, manifestDir = ".jig") {
348
557
  const path = manifestPath(root, manifestDir);
349
- if (!existsSync3(path)) return null;
558
+ if (!existsSync4(path)) return null;
350
559
  let raw;
351
560
  try {
352
- raw = JSON.parse(readFileSync2(path, "utf8"));
561
+ raw = JSON.parse(readFileSync4(path, "utf8"));
353
562
  } catch {
354
563
  throw new Error(`Jig's manifest at ${path} is not valid JSON. ${REGENERATE_HINT}`);
355
564
  }
@@ -379,12 +588,9 @@ function withLock(lock, fn) {
379
588
  }
380
589
  break;
381
590
  } catch (err) {
382
- if (err.code !== "EEXIST") throw err;
591
+ if (!BUSY.has(err.code ?? "")) throw err;
383
592
  if (lockIsStale(lock) || Date.now() > deadline) {
384
- try {
385
- unlinkSync2(lock);
386
- } catch {
387
- }
593
+ unlinkRetrying(lock);
388
594
  continue;
389
595
  }
390
596
  sleep(10 + Math.random() * 20);
@@ -393,15 +599,25 @@ function withLock(lock, fn) {
393
599
  try {
394
600
  fn();
395
601
  } finally {
602
+ unlinkRetrying(lock);
603
+ }
604
+ }
605
+ var BUSY = /* @__PURE__ */ new Set(["EEXIST", "EPERM", "EACCES", "EBUSY"]);
606
+ function unlinkRetrying(lock) {
607
+ for (let attempt = 0; attempt < 20; attempt++) {
396
608
  try {
397
609
  unlinkSync2(lock);
398
- } catch {
610
+ return;
611
+ } catch (err) {
612
+ const code = err.code ?? "";
613
+ if (code === "ENOENT" || !BUSY.has(code)) return;
614
+ sleep(5 + attempt * 5);
399
615
  }
400
616
  }
401
617
  }
402
618
  function lockIsStale(lock) {
403
619
  try {
404
- const [pid, at] = readFileSync2(lock, "utf8").trim().split(/\s+/).map(Number);
620
+ const [pid, at] = readFileSync4(lock, "utf8").trim().split(/\s+/).map(Number);
405
621
  if (at && Date.now() - at > LOCK_STALE_MS) return true;
406
622
  if (pid && pid !== process.pid) {
407
623
  try {
@@ -421,7 +637,7 @@ function sleep(ms) {
421
637
  }
422
638
  function readManifestQuietly(path) {
423
639
  try {
424
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
640
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
425
641
  if (!isPlainObject(parsed) || !isPlainObject(parsed.files)) return null;
426
642
  return parsed;
427
643
  } catch {
@@ -431,9 +647,9 @@ function readManifestQuietly(path) {
431
647
  function isModified(projectRoot, relPath, m) {
432
648
  const recorded = m.files[relPath];
433
649
  if (!recorded) return false;
434
- const abs = join4(projectRoot, relPath);
435
- if (!existsSync3(abs)) return false;
436
- return checksum(readFileSync2(abs, "utf8")) !== recorded;
650
+ const abs = join6(projectRoot, relPath);
651
+ if (!existsSync4(abs)) return false;
652
+ return checksum(readFileSync4(abs, "utf8")) !== recorded;
437
653
  }
438
654
 
439
655
  // src/template/render.ts
@@ -460,97 +676,16 @@ function renderCommandTable(metadata2) {
460
676
  return ["| Command | Description | Status |", "| --- | --- | --- |", ...rows].join("\n");
461
677
  }
462
678
 
463
- // src/install/vendor.ts
464
- import { posix as posix2 } from "path";
465
- var { join: join5 } = posix2;
466
- var DELIMITERS = {
467
- html: ["<!--", "-->"],
468
- css: ["/*", "*/"]
469
- };
470
- function licencePathFor(relPath) {
471
- const depth = relPath.split("/").length - 1;
472
- return depth === 0 ? "." : Array(depth).fill("..").join("/");
473
- }
474
- function vendorHeader(file, version2, style = "html", licencePath = ".") {
475
- const [open, close] = DELIMITERS[style];
476
- const attribution = licencePath === null ? " Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill." : ` Licensed Apache-2.0. See ${join5(licencePath, "LICENSE")} and ${join5(licencePath, "NOTICE")}.`;
477
- return [
478
- `${open} ${file} \u2014 vendored from Jig v${version2}.`,
479
- attribution,
480
- ` Edit freely: \`jig update\` will not overwrite a file you have changed. ${close}`,
481
- "",
482
- ""
483
- ].join("\n");
484
- }
485
- function findMarkerRegions(existing) {
486
- const regions = [];
487
- let searchFrom = 0;
488
- let openStart = null;
489
- while (true) {
490
- const nextStart = existing.indexOf(BLOCK_START, searchFrom);
491
- const nextEnd = existing.indexOf(BLOCK_END, searchFrom);
492
- if (nextStart === -1 && nextEnd === -1) break;
493
- const endIsNext = nextEnd !== -1 && (nextStart === -1 || nextEnd < nextStart);
494
- if (endIsNext) {
495
- if (openStart !== null) {
496
- regions.push({ start: openStart, end: nextEnd + BLOCK_END.length });
497
- openStart = null;
498
- } else {
499
- regions.push({ start: nextEnd, end: nextEnd + BLOCK_END.length });
500
- }
501
- searchFrom = nextEnd + BLOCK_END.length;
502
- } else {
503
- if (openStart !== null) {
504
- regions.push({ start: openStart, end: openStart + BLOCK_START.length });
505
- }
506
- openStart = nextStart;
507
- searchFrom = nextStart + BLOCK_START.length;
508
- }
509
- }
510
- if (openStart !== null) {
511
- regions.push({ start: openStart, end: existing.length });
512
- }
513
- regions.sort((a, b) => a.start - b.start);
514
- return regions;
515
- }
516
- function stripMarkerRegions(existing) {
517
- const regions = findMarkerRegions(existing);
518
- if (regions.length === 0) {
519
- return { text: existing, insertAt: existing.length, removed: false };
520
- }
521
- let text = "";
522
- let cursor = 0;
523
- let insertAt = -1;
524
- regions.forEach((region, i) => {
525
- let chunk = existing.slice(cursor, region.start);
526
- if (i > 0) chunk = chunk.replace(/^\n/, "");
527
- text += chunk;
528
- if (insertAt === -1) insertAt = text.length;
529
- cursor = region.end;
530
- });
531
- const tail = existing.slice(cursor).replace(/^\n/, "");
532
- text += tail;
533
- return { text, insertAt, removed: true };
534
- }
535
- function upsertBlock(existing, block) {
536
- const { text, insertAt, removed } = stripMarkerRegions(existing);
537
- if (!removed) {
538
- const separator = text.length && !text.endsWith("\n") ? "\n\n" : text.length ? "\n" : "";
539
- return `${text}${separator}${block}`;
540
- }
541
- return `${text.slice(0, insertAt)}${block}${text.slice(insertAt)}`;
542
- }
543
-
544
679
  // src/install/writer.ts
545
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
546
- import { dirname as dirname4, join as join6 } from "path";
680
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
681
+ import { dirname as dirname4, join as join7 } from "path";
547
682
 
548
683
  // src/install/line-endings.ts
549
684
  function dominantEnding(existing) {
550
685
  const crlf = (existing.match(/\r\n/g) ?? []).length;
551
- const lf = (existing.match(/(?<!\r)\n/g) ?? []).length;
552
- if (crlf === 0 && lf === 0) return "\n";
553
- return crlf >= lf ? "\r\n" : "\n";
686
+ const lf5 = (existing.match(/(?<!\r)\n/g) ?? []).length;
687
+ if (crlf === 0 && lf5 === 0) return "\n";
688
+ return crlf >= lf5 ? "\r\n" : "\n";
554
689
  }
555
690
  function matchLineEndings(content, existing) {
556
691
  const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
@@ -567,9 +702,9 @@ function createWriter(root, seed = {}) {
567
702
  return {
568
703
  files,
569
704
  write(key, content) {
570
- const abs = join6(root, ...key.split("/"));
705
+ const abs = join7(root, ...key.split("/"));
571
706
  mkdirSync3(dirname4(abs), { recursive: true });
572
- const existing = existsSync4(abs) ? readFileSync3(abs, "utf8") : void 0;
707
+ const existing = existsSync5(abs) ? readFileSync5(abs, "utf8") : void 0;
573
708
  const toWrite = matchLineEndings(content, existing);
574
709
  writeFileAtomic(abs, toWrite);
575
710
  files[key] = checksum(toWrite);
@@ -579,15 +714,15 @@ function createWriter(root, seed = {}) {
579
714
  }
580
715
  function bundleFiles(packageRoot2, dir) {
581
716
  const ext = dir === "rules" ? ".md" : ".css";
582
- return readdirSync2(join6(packageRoot2, dir)).filter((f) => f.endsWith(ext)).sort();
717
+ return readdirSync2(join7(packageRoot2, dir)).filter((f) => f.endsWith(ext)).sort();
583
718
  }
584
719
 
585
720
  // src/commands/install.ts
586
721
  var ASK_INSTRUCTION = "Ask the user directly in chat, one question at a time, and wait for an answer before continuing.";
587
722
  function buildSkillBody(packageRoot2, rulesPath, version2) {
588
- const template = readFileSync4(join7(packageRoot2, "templates", "SKILL.md.tmpl"), "utf8");
723
+ const template = readFileSync6(join8(packageRoot2, "templates", "SKILL.md.tmpl"), "utf8");
589
724
  const metadata2 = JSON.parse(
590
- readFileSync4(join7(packageRoot2, "templates", "command-metadata.json"), "utf8")
725
+ readFileSync6(join8(packageRoot2, "templates", "command-metadata.json"), "utf8")
591
726
  );
592
727
  return render(template, {
593
728
  command_prefix: "/jig ",
@@ -604,12 +739,12 @@ function buildSkillBody(packageRoot2, rulesPath, version2) {
604
739
  }
605
740
  function buildCommandBody(packageRoot2, rulesPath, version2, argsPlaceholder) {
606
741
  const metadata2 = JSON.parse(
607
- readFileSync4(join7(packageRoot2, "templates", "command-metadata.json"), "utf8")
742
+ readFileSync6(join8(packageRoot2, "templates", "command-metadata.json"), "utf8")
608
743
  );
609
744
  const subcommands = Object.entries(metadata2).filter(([, meta]) => meta.status === "available" || meta.status === "agent").map(([name]) => name).sort();
610
- const templatePath = join7(packageRoot2, "templates", "COMMAND.md.tmpl");
611
- if (!existsSync5(templatePath)) return { body: "", subcommands };
612
- const template = readFileSync4(templatePath, "utf8");
745
+ const templatePath = join8(packageRoot2, "templates", "COMMAND.md.tmpl");
746
+ if (!existsSync6(templatePath)) return { body: "", subcommands };
747
+ const template = readFileSync6(templatePath, "utf8");
613
748
  const body = template.replace(/\{\{command_prefix\}\}/g, "/jig ").replace(/\{\{args_placeholder\}\}/g, argsPlaceholder).replace(/\{\{subcommand_list\}\}/g, subcommands.join(", ")).replace(/\{\{scripts_path\}\}/g, cliInvocation(version2)).replace(/\{\{update_path\}\}/g, "npx jig-ui@latest").replace(/\{\{rules_path\}\}/g, rulesPath).replace(/\{\{config_file\}\}/g, "jig.config.json");
614
749
  const unresolved = [...new Set(body.match(/\{\{[a-z_]+\}\}/g) ?? [])];
615
750
  if (unresolved.length > 0) {
@@ -637,6 +772,7 @@ function install(opts) {
637
772
  written: [],
638
773
  skipped: [],
639
774
  stopHook: installStopHook(opts.projectRoot, opts.version),
775
+ agentFiles: ensureAgentFiles(opts.projectRoot, { claude: true }),
640
776
  hookOnly: `Jig's skill is installed globally for 'claude' (${globalReferenceDir} under your home directory), so it stays there. Added only this project's Stop hook.`
641
777
  };
642
778
  }
@@ -657,9 +793,9 @@ function install(opts) {
657
793
  existingManifest = null;
658
794
  }
659
795
  const planned = [];
660
- const rulesDir = join7(opts.packageRoot, "rules");
796
+ const rulesDir = join8(opts.packageRoot, "rules");
661
797
  for (const file of bundleFiles(opts.packageRoot, "rules")) {
662
- const body = readFileSync4(join7(rulesDir, file), "utf8");
798
+ const body = readFileSync6(join8(rulesDir, file), "utf8");
663
799
  planned.push({
664
800
  key: relKey(referenceDir, "rules", file),
665
801
  content: vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + body,
@@ -675,13 +811,13 @@ function install(opts) {
675
811
  }
676
812
  planned.push({
677
813
  key: relKey(referenceDir, "rules.index.json"),
678
- content: readFileSync4(join7(opts.packageRoot, "rules.index.json"), "utf8"),
814
+ content: readFileSync6(join8(opts.packageRoot, "rules.index.json"), "utf8"),
679
815
  checkable: true
680
816
  });
681
817
  for (const src of ["LICENSE", "NOTICE"]) {
682
818
  planned.push({
683
819
  key: relKey(referenceDir, src),
684
- content: readFileSync4(join7(opts.packageRoot, src), "utf8"),
820
+ content: readFileSync6(join8(opts.packageRoot, src), "utf8"),
685
821
  checkable: false
686
822
  });
687
823
  }
@@ -703,9 +839,9 @@ function install(opts) {
703
839
  });
704
840
  for (const file of skillFiles) {
705
841
  const key = file.relPath;
706
- const abs = join7(installRoot, ...key.split("/"));
842
+ const abs = join8(installRoot, ...key.split("/"));
707
843
  const isBlockFile = file.content.includes(BLOCK_START);
708
- const content = isBlockFile && existsSync5(abs) ? upsertBlock(readFileSync4(abs, "utf8"), file.content) : file.content;
844
+ const content = isBlockFile && existsSync6(abs) ? upsertBlock(readFileSync6(abs, "utf8"), file.content) : file.content;
709
845
  planned.push({ key, content, checkable: !isBlockFile });
710
846
  }
711
847
  const writer = createWriter(installRoot);
@@ -732,11 +868,12 @@ function install(opts) {
732
868
  writeManifest(installRoot, manifest, referenceDir);
733
869
  written.push(relKey(referenceDir, "manifest.json"));
734
870
  const stopHook = opts.hook && opts.agent === "claude" && opts.scope === "project" ? installStopHook(installRoot, opts.version) : void 0;
735
- return { written, skipped, stopHook };
871
+ const agentFiles = opts.scope === "project" ? ensureAgentFiles(opts.projectRoot, { claude: opts.agent === "claude" || claudeInstalled(opts.projectRoot, opts.homeDir) }) : [];
872
+ return { written, skipped, stopHook, agentFiles };
736
873
  }
737
874
  function hasStopHook(projectRoot) {
738
875
  try {
739
- const settings = readFileSync4(join7(projectRoot, ".claude", "settings.json"), "utf8");
876
+ const settings = readFileSync6(join8(projectRoot, ".claude", "settings.json"), "utf8");
740
877
  const hooks = JSON.parse(settings).hooks?.Stop ?? [];
741
878
  return hooks.some((group) => (group.hooks ?? []).some((h) => JIG_GATE_HOOK.test(h.command ?? "")));
742
879
  } catch {
@@ -744,11 +881,11 @@ function hasStopHook(projectRoot) {
744
881
  }
745
882
  }
746
883
  function installStopHook(projectRoot, version2) {
747
- const path = join7(projectRoot, ".claude", "settings.json");
884
+ const path = join8(projectRoot, ".claude", "settings.json");
748
885
  let settings = {};
749
- if (existsSync5(path)) {
886
+ if (existsSync6(path)) {
750
887
  try {
751
- settings = JSON.parse(readFileSync4(path, "utf8"));
888
+ settings = JSON.parse(readFileSync6(path, "utf8"));
752
889
  } catch {
753
890
  return false;
754
891
  }
@@ -759,14 +896,14 @@ function installStopHook(projectRoot, version2) {
759
896
  const stop = (hooks.Stop ?? []).map((group) => ({ ...group, hooks: group.hooks.filter((h) => !isJig(h)) })).filter((group) => group.hooks.length > 0);
760
897
  stop.push({ hooks: [{ type: "command", command }] });
761
898
  settings.hooks = { ...hooks, Stop: stop };
762
- mkdirSync4(join7(projectRoot, ".claude"), { recursive: true });
899
+ mkdirSync4(join8(projectRoot, ".claude"), { recursive: true });
763
900
  writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
764
901
  return true;
765
902
  }
766
903
 
767
904
  // src/commands/update.ts
768
- import { join as join10 } from "path";
769
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
905
+ import { join as join11 } from "path";
906
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
770
907
 
771
908
  // src/install/target.ts
772
909
  import { resolve } from "path";
@@ -809,14 +946,14 @@ function resolveAllInstalled(projectRoot, homeDir) {
809
946
  }
810
947
 
811
948
  // src/init/state.ts
812
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5 } from "fs";
813
- import { dirname as dirname5, join as join8 } from "path";
814
- var INIT_MANIFEST_REL = join8(".jig", "state.json");
949
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7 } from "fs";
950
+ import { dirname as dirname5, join as join9 } from "path";
951
+ var INIT_MANIFEST_REL = join9(".jig", "state.json");
815
952
  function readInitManifest(projectRoot) {
816
- const path = join8(projectRoot, INIT_MANIFEST_REL);
817
- if (!existsSync6(path)) return null;
953
+ const path = join9(projectRoot, INIT_MANIFEST_REL);
954
+ if (!existsSync7(path)) return null;
818
955
  try {
819
- const raw = JSON.parse(readFileSync5(path, "utf8"));
956
+ const raw = JSON.parse(readFileSync7(path, "utf8"));
820
957
  if (typeof raw !== "object" || raw === null) return null;
821
958
  const { files, version: version2, modes } = raw;
822
959
  if (typeof files !== "object" || files === null) return null;
@@ -831,7 +968,7 @@ function readInitManifest(projectRoot) {
831
968
  }
832
969
  var MERGE_ATTEMPTS = 5;
833
970
  function writeInitManifest(projectRoot, m) {
834
- const path = join8(projectRoot, INIT_MANIFEST_REL);
971
+ const path = join9(projectRoot, INIT_MANIFEST_REL);
835
972
  mkdirSync5(dirname5(path), { recursive: true });
836
973
  for (let attempt = 0; attempt < MERGE_ATTEMPTS; attempt++) {
837
974
  const onDisk = readInitManifest(projectRoot);
@@ -852,15 +989,15 @@ function isInitFileModified(projectRoot, relPath, m) {
852
989
  }
853
990
 
854
991
  // src/init/migrate.ts
855
- import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync3, rmSync } from "fs";
856
- import { join as join9 } from "path";
992
+ import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync3, rmSync } from "fs";
993
+ import { join as join10 } from "path";
857
994
  var LEGACY_RULE_RE = /^\d{2}-.*\.md$/;
858
995
  function isLegacyInstallArtifact(name) {
859
996
  return LEGACY_RULE_RE.test(name) || name === "rules.index.json" || name === "LICENSE" || name === "NOTICE" || name === "manifest.json";
860
997
  }
861
998
  function readLegacyManifest(dirAbs) {
862
999
  try {
863
- const raw = JSON.parse(readFileSync6(join9(dirAbs, "manifest.json"), "utf8"));
1000
+ const raw = JSON.parse(readFileSync8(join10(dirAbs, "manifest.json"), "utf8"));
864
1001
  if (raw && typeof raw === "object" && typeof raw.files === "object") {
865
1002
  return raw;
866
1003
  }
@@ -872,8 +1009,8 @@ function classify(projectRoot, relPath, legacyManifest) {
872
1009
  return legacyManifest && relPath in legacyManifest.files ? isModified(projectRoot, relPath, legacyManifest) : "unknown";
873
1010
  }
874
1011
  function detectLegacyRules(projectRoot) {
875
- const jigDir = join9(projectRoot, ".jig");
876
- if (!existsSync7(jigDir)) return { present: false, files: [] };
1012
+ const jigDir = join10(projectRoot, ".jig");
1013
+ if (!existsSync8(jigDir)) return { present: false, files: [] };
877
1014
  const legacyManifest = readLegacyManifest(jigDir);
878
1015
  let entries;
879
1016
  try {
@@ -889,12 +1026,12 @@ function detectLegacyRules(projectRoot) {
889
1026
  }
890
1027
  function detectLegacyCursorRules(projectRoot) {
891
1028
  const bundleDirRel = ".cursor/rules/jig";
892
- const bundleDir = join9(projectRoot, ...bundleDirRel.split("/"));
1029
+ const bundleDir = join10(projectRoot, ...bundleDirRel.split("/"));
893
1030
  const mdcRel = ".cursor/rules/jig.mdc";
894
- const mdcAbs = join9(projectRoot, ...mdcRel.split("/"));
1031
+ const mdcAbs = join10(projectRoot, ...mdcRel.split("/"));
895
1032
  const legacyManifest = readLegacyManifest(bundleDir);
896
1033
  const files = [];
897
- if (existsSync7(mdcAbs)) {
1034
+ if (existsSync8(mdcAbs)) {
898
1035
  files.push({ relPath: mdcRel, modified: classify(projectRoot, mdcRel, legacyManifest) });
899
1036
  }
900
1037
  let entries;
@@ -948,7 +1085,7 @@ function removableLegacyFiles(report2) {
948
1085
  function removeLegacyFiles(projectRoot, relPaths) {
949
1086
  for (const relPath of relPaths) {
950
1087
  try {
951
- rmSync(join9(projectRoot, ...relPath.split("/")), { force: true });
1088
+ rmSync(join10(projectRoot, ...relPath.split("/")), { force: true });
952
1089
  } catch {
953
1090
  }
954
1091
  }
@@ -991,6 +1128,9 @@ function update(opts) {
991
1128
  const initResult = updateInitFiles(opts);
992
1129
  updated.push(...initResult.updated);
993
1130
  skipped.push(...initResult.skipped);
1131
+ if (existsSync9(join11(opts.projectRoot, ".jig", "state.json"))) {
1132
+ updated.push(...ensureAgentFiles(opts.projectRoot, { claude: claudeInstalled(opts.projectRoot, opts.homeDir) }));
1133
+ }
994
1134
  return {
995
1135
  updated,
996
1136
  skipped,
@@ -1013,14 +1153,14 @@ function updateTarget(opts, resolved) {
1013
1153
  const write = (key, content) => {
1014
1154
  updated.push(writer.write(key, content));
1015
1155
  };
1016
- const rulesDir = join10(opts.packageRoot, "rules");
1156
+ const rulesDir = join11(opts.packageRoot, "rules");
1017
1157
  for (const file of bundleFiles(opts.packageRoot, "rules")) {
1018
1158
  const key = relKey(referenceDir, "rules", file);
1019
1159
  if (isModified(installRoot, key, existing)) {
1020
1160
  skipped.push(key);
1021
1161
  continue;
1022
1162
  }
1023
- write(key, vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + readFileSync7(join10(rulesDir, file), "utf8"));
1163
+ write(key, vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + readFileSync9(join11(rulesDir, file), "utf8"));
1024
1164
  }
1025
1165
  for (const ref of referenceFiles(opts.packageRoot)) {
1026
1166
  const key = relKey(referenceDir, ...ref.relPath.split("/"));
@@ -1034,7 +1174,7 @@ function updateTarget(opts, resolved) {
1034
1174
  if (isModified(installRoot, indexKey, existing)) {
1035
1175
  skipped.push(indexKey);
1036
1176
  } else {
1037
- write(indexKey, readFileSync7(join10(opts.packageRoot, "rules.index.json"), "utf8"));
1177
+ write(indexKey, readFileSync9(join11(opts.packageRoot, "rules.index.json"), "utf8"));
1038
1178
  }
1039
1179
  const rulesPath = rulesPathFor(referenceDir, discoveredScope);
1040
1180
  const skillBody = buildSkillBody(opts.packageRoot, rulesPath, opts.version);
@@ -1056,8 +1196,8 @@ function updateTarget(opts, resolved) {
1056
1196
  const key = file.relPath;
1057
1197
  const isBlockFile = file.content.includes(BLOCK_START);
1058
1198
  if (isBlockFile) {
1059
- const abs = join10(installRoot, ...key.split("/"));
1060
- const current = existsSync8(abs) ? readFileSync7(abs, "utf8") : "";
1199
+ const abs = join11(installRoot, ...key.split("/"));
1200
+ const current = existsSync9(abs) ? readFileSync9(abs, "utf8") : "";
1061
1201
  write(key, upsertBlock(current, file.content));
1062
1202
  continue;
1063
1203
  }
@@ -1068,7 +1208,7 @@ function updateTarget(opts, resolved) {
1068
1208
  write(key, file.content);
1069
1209
  }
1070
1210
  for (const src of ["LICENSE", "NOTICE"]) {
1071
- write(relKey(referenceDir, src), readFileSync7(join10(opts.packageRoot, src), "utf8"));
1211
+ write(relKey(referenceDir, src), readFileSync9(join11(opts.packageRoot, src), "utf8"));
1072
1212
  }
1073
1213
  const manifest = {
1074
1214
  ...existing,
@@ -1084,7 +1224,7 @@ function updateInitFiles(opts) {
1084
1224
  const updated = [];
1085
1225
  const skipped = [];
1086
1226
  const tokens = [];
1087
- const tokensDir = join10(opts.packageRoot, "tokens");
1227
+ const tokensDir = join11(opts.packageRoot, "tokens");
1088
1228
  const initManifest = readInitManifest(opts.projectRoot);
1089
1229
  if (initManifest) {
1090
1230
  const initFiles = { ...initManifest.files };
@@ -1096,10 +1236,10 @@ function updateInitFiles(opts) {
1096
1236
  for (const key of keys) {
1097
1237
  let before = "";
1098
1238
  try {
1099
- before = readFileSync7(join10(opts.projectRoot, ...key.split("/")), "utf8");
1239
+ before = readFileSync9(join11(opts.projectRoot, ...key.split("/")), "utf8");
1100
1240
  } catch {
1101
1241
  }
1102
- const fresh = readFileSync7(join10(tokensDir, file), "utf8");
1242
+ const fresh = readFileSync9(join11(tokensDir, file), "utf8");
1103
1243
  if (isInitFileModified(opts.projectRoot, key, initManifest)) {
1104
1244
  skipped.push(key);
1105
1245
  const change2 = tokenChange(key, before, fresh, true);
@@ -1120,12 +1260,12 @@ function updateInitFiles(opts) {
1120
1260
  }
1121
1261
 
1122
1262
  // src/commands/explain.ts
1123
- import { existsSync as existsSync9 } from "fs";
1124
- import { join as join15 } from "path";
1263
+ import { existsSync as existsSync10 } from "fs";
1264
+ import { join as join16 } from "path";
1125
1265
 
1126
1266
  // src/rules/load.ts
1127
- import { readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
1128
- import { join as join11 } from "path";
1267
+ import { readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
1268
+ import { join as join12 } from "path";
1129
1269
 
1130
1270
  // src/rules/parse.ts
1131
1271
  var HEADING = /^###\s+([A-Z])-(\d+)\s+(.+?)\s*$/;
@@ -1221,9 +1361,9 @@ function loadRules(rulesDir, indexPath) {
1221
1361
  const files = readdirSync4(rulesDir).filter((f) => f.endsWith(".md")).sort();
1222
1362
  const parsed = [];
1223
1363
  for (const file of files) {
1224
- parsed.push(...parseRules(readFileSync8(join11(rulesDir, file), "utf8"), file));
1364
+ parsed.push(...parseRules(readFileSync10(join12(rulesDir, file), "utf8"), file));
1225
1365
  }
1226
- const entries = validateIndex(JSON.parse(readFileSync8(indexPath, "utf8")));
1366
+ const entries = validateIndex(JSON.parse(readFileSync10(indexPath, "utf8")));
1227
1367
  const byId = new Map(entries.map((e) => [e.id, e]));
1228
1368
  const missing = parsed.filter((r) => !byId.has(r.id)).map((r) => r.id);
1229
1369
  if (missing.length) {
@@ -1245,8 +1385,8 @@ function loadRules(rulesDir, indexPath) {
1245
1385
  }
1246
1386
 
1247
1387
  // src/rules/specs.ts
1248
- import { readFileSync as readFileSync9, readdirSync as readdirSync5 } from "fs";
1249
- import { join as join12 } from "path";
1388
+ import { readFileSync as readFileSync11, readdirSync as readdirSync5 } from "fs";
1389
+ import { join as join13 } from "path";
1250
1390
  var KINDS = {
1251
1391
  P: "pattern",
1252
1392
  M: "mode",
@@ -1305,14 +1445,14 @@ function parseSpecs(markdown, sourceFile) {
1305
1445
  function loadSpecs(rulesDir) {
1306
1446
  const specs = [];
1307
1447
  for (const file of readdirSync5(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
1308
- specs.push(...parseSpecs(readFileSync9(join12(rulesDir, file), "utf8"), file));
1448
+ specs.push(...parseSpecs(readFileSync11(join13(rulesDir, file), "utf8"), file));
1309
1449
  }
1310
1450
  return specs;
1311
1451
  }
1312
1452
 
1313
1453
  // src/rules/sections.ts
1314
- import { readFileSync as readFileSync10, readdirSync as readdirSync6 } from "fs";
1315
- import { join as join13 } from "path";
1454
+ import { readFileSync as readFileSync12, readdirSync as readdirSync6 } from "fs";
1455
+ import { join as join14 } from "path";
1316
1456
  var RULE_HEADING = /^###\s+[A-Z]-\d+\b/;
1317
1457
  var SPEC_HEADING = /^##\s+[PMLRT]-\d+\b/;
1318
1458
  var ANY_HEADING = /^(#{1,6})\s+(.*?)\s*$/;
@@ -1353,16 +1493,16 @@ function parseSections(markdown, file) {
1353
1493
  function loadSections(rulesDir) {
1354
1494
  const out = [];
1355
1495
  for (const file of readdirSync6(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
1356
- out.push(...parseSections(readFileSync10(join13(rulesDir, file), "utf8"), file));
1496
+ out.push(...parseSections(readFileSync12(join14(rulesDir, file), "utf8"), file));
1357
1497
  }
1358
1498
  return out;
1359
1499
  }
1360
1500
 
1361
1501
  // src/rules/layers.ts
1362
- import { readFileSync as readFileSync11 } from "fs";
1363
- import { join as join14 } from "path";
1502
+ import { readFileSync as readFileSync13 } from "fs";
1503
+ import { join as join15 } from "path";
1364
1504
  function loadLayers(packageRoot2) {
1365
- return JSON.parse(readFileSync11(join14(packageRoot2, "layers.json"), "utf8"));
1505
+ return JSON.parse(readFileSync13(join15(packageRoot2, "layers.json"), "utf8"));
1366
1506
  }
1367
1507
 
1368
1508
  // src/commands/explain.ts
@@ -1444,8 +1584,8 @@ function renderSection(s) {
1444
1584
  function explain(opts) {
1445
1585
  const raw = opts.ruleId.trim().toUpperCase();
1446
1586
  const root = opts.packageRoot ?? assetRoot();
1447
- const rulesDir = join15(root, "rules");
1448
- const rules = loadRules(rulesDir, join15(root, "rules.index.json"));
1587
+ const rulesDir = join16(root, "rules");
1588
+ const rules = loadRules(rulesDir, join16(root, "rules.index.json"));
1449
1589
  const specs = loadSpecs(rulesDir);
1450
1590
  const sections = loadSections(rulesDir);
1451
1591
  const entries = searchEntries(rules, specs, sections);
@@ -1501,8 +1641,8 @@ function explain(opts) {
1501
1641
  const id = `${shape[1]}-${shape[2]}`;
1502
1642
  const rule = rules.find((r) => r.id === id);
1503
1643
  if (rule) {
1504
- const example = join15(root, "examples", `${id}.html`);
1505
- return existsSync9(example) ? `${renderRule(rule)}
1644
+ const example = join16(root, "examples", `${id}.html`);
1645
+ return existsSync10(example) ? `${renderRule(rule)}
1506
1646
 
1507
1647
  Example: ${example} (a dont and a do, each a self-contained HTML fragment)` : renderRule(rule);
1508
1648
  }
@@ -1559,13 +1699,13 @@ function sectionsOf(entries) {
1559
1699
  }
1560
1700
 
1561
1701
  // src/commands/check.ts
1562
- import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
1563
- import { join as join24 } from "path";
1702
+ import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
1703
+ import { join as join25 } from "path";
1564
1704
 
1565
1705
  // src/check/files.ts
1566
1706
  import { execFileSync } from "child_process";
1567
1707
  import { readdirSync as readdirSync7, realpathSync, statSync as statSync2 } from "fs";
1568
- import { join as join16, relative, sep } from "path";
1708
+ import { join as join17, relative, sep } from "path";
1569
1709
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1570
1710
  "node_modules",
1571
1711
  "dist",
@@ -1640,7 +1780,7 @@ function walk(root, dir, out, seen) {
1640
1780
  }
1641
1781
  for (const entry of entries) {
1642
1782
  if (EXCLUDE_DIRS.has(entry.name)) continue;
1643
- const abs = join16(dir, entry.name);
1783
+ const abs = join17(dir, entry.name);
1644
1784
  let isDir = entry.isDirectory();
1645
1785
  let isFile = entry.isFile();
1646
1786
  if (entry.isSymbolicLink()) {
@@ -1782,6 +1922,10 @@ function formatReport(findings, meta) {
1782
1922
  ` Scope: files changed since HEAD, not the whole project \u2014 this is the inner-loop default. A clean result here means nothing in your diff fired, not that the project is clean. Run 'jig check --all' for that.`
1783
1923
  );
1784
1924
  }
1925
+ for (const problem of meta.agentFiles ?? []) lines.push(` ${problem}`);
1926
+ for (const { entry, why } of meta.exemptIgnored ?? []) {
1927
+ lines.push(` jig.config.json exempt: "${entry}" is ignored: ${why}`);
1928
+ }
1785
1929
  if (meta.exemptPatterns && meta.exemptPatterns.length > 0) {
1786
1930
  const n = meta.exempt?.length ?? 0;
1787
1931
  lines.push(` ${n} file(s) exempt via jig.config.json and not scanned:`);
@@ -1789,6 +1933,9 @@ function formatReport(findings, meta) {
1789
1933
  const note = count === 0 ? meta.scope === "changed" ? "matches nothing among the changed files \u2014 not necessarily wrong" : "matches nothing \u2014 check the path" : tooBroad ? `${count} files \u2014 likely too broad, review it` : `${count} file${count > 1 ? "s" : ""}`;
1790
1934
  lines.push(` ${pattern} (${note})`);
1791
1935
  }
1936
+ for (const { pattern, suggest } of meta.exemptPatterns) {
1937
+ if (suggest?.length) lines.push(` ${pattern}: did you mean ${suggest.map((f) => `"${f}"`).join(" or ")}? Paths start at the project folder.`);
1938
+ }
1792
1939
  if (n > 0) {
1793
1940
  lines.push(` ${meta.exempt.slice(0, 8).join(", ")}${n > 8 ? `, and ${n - 8} more` : ""}`);
1794
1941
  }
@@ -1952,8 +2099,8 @@ function isReaderText(file) {
1952
2099
  }
1953
2100
 
1954
2101
  // src/check/run.ts
1955
- import { readFileSync as readFileSync13 } from "fs";
1956
- import { join as join18 } from "path";
2102
+ import { readFileSync as readFileSync15 } from "fs";
2103
+ import { join as join19 } from "path";
1957
2104
 
1958
2105
  // src/check/css.ts
1959
2106
  function maskComments(source) {
@@ -3372,8 +3519,8 @@ var menuState = {
3372
3519
  };
3373
3520
 
3374
3521
  // src/check/declared-properties.ts
3375
- import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
3376
- import { join as join17 } from "path";
3522
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
3523
+ import { join as join18 } from "path";
3377
3524
  var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
3378
3525
  var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
3379
3526
  var SET_PROPERTY_RE = /setProperty\(\s*['"`](--[\w-]+)/g;
@@ -3385,16 +3532,16 @@ function addAll(names, text) {
3385
3532
  }
3386
3533
  }
3387
3534
  function readPackageImport(projectRoot, spec) {
3388
- const base = join17(projectRoot, "node_modules", spec);
3535
+ const base = join18(projectRoot, "node_modules", spec);
3389
3536
  for (const candidate of [base, `${base}.css`]) {
3390
3537
  try {
3391
- if (existsSync10(candidate) && candidate.endsWith(".css")) return readFileSync12(candidate, "utf8");
3538
+ if (existsSync11(candidate) && candidate.endsWith(".css")) return readFileSync14(candidate, "utf8");
3392
3539
  } catch {
3393
3540
  }
3394
3541
  }
3395
3542
  try {
3396
- const pkg = JSON.parse(readFileSync12(join17(base, "package.json"), "utf8"));
3397
- if (pkg.style) return readFileSync12(join17(base, pkg.style), "utf8");
3543
+ const pkg = JSON.parse(readFileSync14(join18(base, "package.json"), "utf8"));
3544
+ if (pkg.style) return readFileSync14(join18(base, pkg.style), "utf8");
3398
3545
  } catch {
3399
3546
  }
3400
3547
  return void 0;
@@ -3406,7 +3553,7 @@ function collectDeclaredProperties(projectRoot, files, tokenNames = []) {
3406
3553
  for (const file of files) {
3407
3554
  let raw;
3408
3555
  try {
3409
- raw = readFileSync12(join17(projectRoot, file), "utf8");
3556
+ raw = readFileSync14(join18(projectRoot, file), "utf8");
3410
3557
  } catch {
3411
3558
  continue;
3412
3559
  }
@@ -4256,7 +4403,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
4256
4403
  if (applicable.length === 0) continue;
4257
4404
  let raw;
4258
4405
  try {
4259
- raw = readFileSync13(join18(projectRoot, file), "utf8");
4406
+ raw = readFileSync15(join19(projectRoot, file), "utf8");
4260
4407
  } catch {
4261
4408
  continue;
4262
4409
  }
@@ -4270,8 +4417,8 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
4270
4417
  }
4271
4418
 
4272
4419
  // src/check/tokens.ts
4273
- import { existsSync as existsSync11, readdirSync as readdirSync8, readFileSync as readFileSync14 } from "fs";
4274
- import { join as join19 } from "path";
4420
+ import { existsSync as existsSync12, readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
4421
+ import { join as join20 } from "path";
4275
4422
  function rootDeclarations(source) {
4276
4423
  const out = {};
4277
4424
  for (const block of splitRuleBlocks(source)) {
@@ -4286,20 +4433,20 @@ function rootDeclarations(source) {
4286
4433
  }
4287
4434
  function loadTokenMap(projectRoot, consumerFiles = []) {
4288
4435
  const jig = {};
4289
- const tokensDir = join19(projectRoot, ".jig", "tokens");
4290
- if (existsSync11(tokensDir)) {
4436
+ const tokensDir = join20(projectRoot, ".jig", "tokens");
4437
+ if (existsSync12(tokensDir)) {
4291
4438
  for (const file of readdirSync8(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
4292
- Object.assign(jig, rootDeclarations(readFileSync14(join19(tokensDir, file), "utf8")));
4439
+ Object.assign(jig, rootDeclarations(readFileSync16(join20(tokensDir, file), "utf8")));
4293
4440
  }
4294
4441
  }
4295
4442
  const consumer = {};
4296
4443
  const ambiguous = /* @__PURE__ */ new Set();
4297
- const vendored = join19(".jig", "tokens");
4444
+ const vendored = join20(".jig", "tokens");
4298
4445
  for (const file of consumerFiles) {
4299
4446
  if (file.includes(vendored)) continue;
4300
4447
  let source;
4301
4448
  try {
4302
- source = readFileSync14(join19(projectRoot, file), "utf8");
4449
+ source = readFileSync16(join20(projectRoot, file), "utf8");
4303
4450
  } catch {
4304
4451
  continue;
4305
4452
  }
@@ -4318,8 +4465,8 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
4318
4465
  }
4319
4466
 
4320
4467
  // src/check/exempt.ts
4321
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
4322
- import { join as join20 } from "path";
4468
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
4469
+ import { join as join21 } from "path";
4323
4470
  function globToRegExp(glob) {
4324
4471
  let out = "";
4325
4472
  for (let i = 0; i < glob.length; i++) {
@@ -4338,18 +4485,37 @@ function globToRegExp(glob) {
4338
4485
  return new RegExp(`^${out}$`);
4339
4486
  }
4340
4487
  function readExemptions(projectRoot) {
4341
- const configPath = join20(projectRoot, "jig.config.json");
4342
- if (!existsSync12(configPath)) return [];
4488
+ const none = { patterns: [], ignored: [] };
4489
+ const configPath = join21(projectRoot, "jig.config.json");
4490
+ if (!existsSync13(configPath)) return none;
4343
4491
  let declared;
4344
4492
  try {
4345
- declared = JSON.parse(readFileSync15(configPath, "utf8")).exempt;
4493
+ declared = JSON.parse(readFileSync17(configPath, "utf8")).exempt;
4346
4494
  } catch {
4347
- return [];
4495
+ return none;
4496
+ }
4497
+ if (!Array.isArray(declared)) return none;
4498
+ const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
4499
+ const patterns = [];
4500
+ const ignored = [];
4501
+ for (const raw of declared) {
4502
+ if (typeof raw !== "string" || !raw.trim()) continue;
4503
+ let p = raw.trim().replace(/\\/g, "/");
4504
+ if (p.startsWith(`${root}/`)) p = p.slice(root.length + 1);
4505
+ p = p.replace(/^(\.\/)+/, "");
4506
+ if (p.startsWith("/") || /^[A-Za-z]:\//.test(p)) {
4507
+ ignored.push({ entry: raw, why: "an absolute path outside this project. Write it from the project folder, where jig.config.json is: `src/\u2026`" });
4508
+ } else if (p.split("/").includes("..")) {
4509
+ ignored.push({ entry: raw, why: "it leaves the project folder (`..`). Write it from the project folder, where jig.config.json is: `src/\u2026`" });
4510
+ } else {
4511
+ patterns.push(p);
4512
+ }
4348
4513
  }
4349
- if (!Array.isArray(declared)) return [];
4350
- return declared.filter(
4351
- (p) => typeof p === "string" && p.trim() !== "" && !p.startsWith("/") && !p.split("/").includes("..")
4352
- );
4514
+ return { patterns, ignored };
4515
+ }
4516
+ function suggestPaths(pattern, files) {
4517
+ if (pattern.includes("/") || /[*?]/.test(pattern)) return [];
4518
+ return files.filter((f) => f === pattern || f.endsWith(`/${pattern}`)).slice(0, 3);
4353
4519
  }
4354
4520
  var LIKELY_TOO_BROAD = 3;
4355
4521
  function applyExemptions(files, patterns) {
@@ -4374,8 +4540,8 @@ function applyExemptions(files, patterns) {
4374
4540
  }
4375
4541
 
4376
4542
  // src/check/waiver.ts
4377
- import { readFileSync as readFileSync16 } from "fs";
4378
- import { join as join21 } from "path";
4543
+ import { readFileSync as readFileSync18 } from "fs";
4544
+ import { join as join22 } from "path";
4379
4545
  var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
4380
4546
  function readWaiver(line) {
4381
4547
  const m = WAIVER.exec(line);
@@ -4390,7 +4556,7 @@ function applyWaivers(root, findings) {
4390
4556
  let cached = lines.get(file);
4391
4557
  if (!cached) {
4392
4558
  try {
4393
- cached = readFileSync16(join21(root, file), "utf8").split("\n");
4559
+ cached = readFileSync18(join22(root, file), "utf8").split("\n");
4394
4560
  } catch {
4395
4561
  cached = [];
4396
4562
  }
@@ -4455,8 +4621,8 @@ function hasMenuToggle(raw) {
4455
4621
  }
4456
4622
 
4457
4623
  // src/check/mode-wiring.ts
4458
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
4459
- import { dirname as dirname6, join as join22 } from "path";
4624
+ import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
4625
+ import { dirname as dirname6, join as join23 } from "path";
4460
4626
  var MODES = ["editorial", "product", "operator"];
4461
4627
  function configProblems(config) {
4462
4628
  const problems = [];
@@ -4478,10 +4644,10 @@ function configProblems(config) {
4478
4644
  }
4479
4645
  function modeWiringProblems(projectRoot) {
4480
4646
  let config;
4481
- const path = join22(projectRoot, "jig.config.json");
4482
- if (!existsSync13(path)) return [];
4647
+ const path = join23(projectRoot, "jig.config.json");
4648
+ if (!existsSync14(path)) return [];
4483
4649
  try {
4484
- config = JSON.parse(readFileSync17(path, "utf8"));
4650
+ config = JSON.parse(readFileSync19(path, "utf8"));
4485
4651
  } catch {
4486
4652
  return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
4487
4653
  }
@@ -4494,10 +4660,10 @@ function modeWiringProblems(projectRoot) {
4494
4660
  const rel = (f) => dir === "." ? f : `${dir}/${f}`;
4495
4661
  const multi = modes.length > 1;
4496
4662
  const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
4497
- const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync13(join22(projectRoot, rel(f))));
4663
+ const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync14(join23(projectRoot, rel(f))));
4498
4664
  if (!anyBarrel) return [];
4499
4665
  const problems = [];
4500
- if (multi && existsSync13(join22(projectRoot, rel("theme.css")))) {
4666
+ if (multi && existsSync14(join23(projectRoot, rel("theme.css")))) {
4501
4667
  problems.push({
4502
4668
  mode: modes[0],
4503
4669
  barrel: rel("theme.css"),
@@ -4509,7 +4675,7 @@ function modeWiringProblems(projectRoot) {
4509
4675
  const modeFile = `mode.${mode}.css`;
4510
4676
  let body;
4511
4677
  try {
4512
- body = readFileSync17(join22(projectRoot, barrel), "utf8");
4678
+ body = readFileSync19(join23(projectRoot, barrel), "utf8");
4513
4679
  } catch {
4514
4680
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
4515
4681
  return;
@@ -4521,7 +4687,7 @@ function modeWiringProblems(projectRoot) {
4521
4687
  barrel,
4522
4688
  message: `jig.config.json declares ${mode}, but ${barrel} imports ${imported ? `mode.${imported}.css` : "no mode file"} \u2014 the page is styled at ${imported ?? "no"} density`
4523
4689
  });
4524
- } else if (!existsSync13(join22(projectRoot, rel(modeFile)))) {
4690
+ } else if (!existsSync14(join23(projectRoot, rel(modeFile)))) {
4525
4691
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
4526
4692
  }
4527
4693
  });
@@ -4529,8 +4695,8 @@ function modeWiringProblems(projectRoot) {
4529
4695
  }
4530
4696
 
4531
4697
  // src/check/token-audit.ts
4532
- import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
4533
- import { join as join23 } from "path";
4698
+ import { existsSync as existsSync15, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
4699
+ import { join as join24 } from "path";
4534
4700
  var TEXT_FLOOR = 4.5;
4535
4701
  var UI_FLOOR = 3;
4536
4702
  var FOREGROUNDS = [
@@ -4586,24 +4752,24 @@ function floorFor(token) {
4586
4752
  function tokenFiles(projectRoot) {
4587
4753
  const read = (rel) => {
4588
4754
  try {
4589
- return readFileSync18(join23(projectRoot, ...rel.split("/")), "utf8");
4755
+ return readFileSync20(join24(projectRoot, ...rel.split("/")), "utf8");
4590
4756
  } catch {
4591
4757
  return null;
4592
4758
  }
4593
4759
  };
4594
- const statePath = join23(projectRoot, ".jig", "state.json");
4595
- if (existsSync14(statePath)) {
4760
+ const statePath = join24(projectRoot, ".jig", "state.json");
4761
+ if (existsSync15(statePath)) {
4596
4762
  try {
4597
- const state = JSON.parse(readFileSync18(statePath, "utf8"));
4763
+ const state = JSON.parse(readFileSync20(statePath, "utf8"));
4598
4764
  const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
4599
4765
  const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
4600
4766
  if (out.length > 0) return out;
4601
4767
  } catch {
4602
4768
  }
4603
4769
  }
4604
- const legacy = join23(projectRoot, ".jig", "tokens");
4605
- if (!existsSync14(legacy)) return [];
4606
- return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync18(join23(legacy, f), "utf8") }));
4770
+ const legacy = join24(projectRoot, ".jig", "tokens");
4771
+ if (!existsSync15(legacy)) return [];
4772
+ return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync20(join24(legacy, f), "utf8") }));
4607
4773
  }
4608
4774
  function auditTokenLayer(projectRoot) {
4609
4775
  const files = tokenFiles(projectRoot);
@@ -4671,15 +4837,15 @@ function auditTokenLayer(projectRoot) {
4671
4837
  }
4672
4838
 
4673
4839
  // src/commands/check.ts
4674
- var LEGACY_INDEX_REL = join24(".jig", "rules.index.json");
4840
+ var LEGACY_INDEX_REL = join25(".jig", "rules.index.json");
4675
4841
  function resolveIndexPath(projectRoot) {
4676
- const legacy = join24(projectRoot, LEGACY_INDEX_REL);
4677
- if (existsSync15(legacy)) return legacy;
4678
- return join24(assetRoot(), "rules.index.json");
4842
+ const legacy = join25(projectRoot, LEGACY_INDEX_REL);
4843
+ if (existsSync16(legacy)) return legacy;
4844
+ return join25(assetRoot(), "rules.index.json");
4679
4845
  }
4680
4846
  function resolveMode(projectRoot) {
4681
4847
  try {
4682
- const config = JSON.parse(readFileSync19(join24(projectRoot, "jig.config.json"), "utf8"));
4848
+ const config = JSON.parse(readFileSync21(join25(projectRoot, "jig.config.json"), "utf8"));
4683
4849
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
4684
4850
  if (modes.length === 1) return modes[0];
4685
4851
  return modes.length > 1 ? "mixed" : "unknown";
@@ -4696,24 +4862,25 @@ function summariseUnscanned(files) {
4696
4862
  }
4697
4863
  function countSpecs() {
4698
4864
  try {
4699
- return loadSpecs(join24(assetRoot(), "rules")).length;
4865
+ return loadSpecs(join25(assetRoot(), "rules")).length;
4700
4866
  } catch {
4701
4867
  return 0;
4702
4868
  }
4703
4869
  }
4704
4870
  function check(opts) {
4705
4871
  const indexPath = resolveIndexPath(opts.projectRoot);
4706
- const index = validateIndex(JSON.parse(readFileSync19(indexPath, "utf8")));
4872
+ const index = validateIndex(JSON.parse(readFileSync21(indexPath, "utf8")));
4707
4873
  const selection = selectFiles(opts.projectRoot, opts.all);
4708
- const exemptions = readExemptions(opts.projectRoot);
4709
- const { scanned: files, exempt, byPattern } = applyExemptions(selection.files, exemptions);
4874
+ const { patterns: exemptions, ignored: exemptIgnored } = readExemptions(opts.projectRoot);
4875
+ const { scanned: files, exempt, byPattern: counted } = applyExemptions(selection.files, exemptions);
4876
+ const byPattern = counted.map((p) => ({ ...p, suggest: p.count === 0 && selection.mode !== "changed" ? suggestPaths(p.pattern, selection.files) : [] }));
4710
4877
  const bucketFilter = opts.ci ? (b) => b === "mechanical" : void 0;
4711
4878
  const stylesheets = selection.mode === "all" ? files : selectFiles(opts.projectRoot, true).files;
4712
4879
  const tokens = loadTokenMap(opts.projectRoot, stylesheets);
4713
4880
  const projectParticipates = stylesheets.some((f) => {
4714
4881
  if (!hasExtension(f, CSS_EXTENSIONS)) return false;
4715
4882
  try {
4716
- return participatesInTokenLayer(readFileSync19(join24(opts.projectRoot, f), "utf8"), tokens);
4883
+ return participatesInTokenLayer(readFileSync21(join25(opts.projectRoot, f), "utf8"), tokens);
4717
4884
  } catch {
4718
4885
  return false;
4719
4886
  }
@@ -4721,7 +4888,7 @@ function check(opts) {
4721
4888
  const projectResponsive = stylesheets.some((f) => {
4722
4889
  if (!isStyleBearing(f)) return false;
4723
4890
  try {
4724
- const raw = readFileSync19(join24(opts.projectRoot, f), "utf8");
4891
+ const raw = readFileSync21(join25(opts.projectRoot, f), "utf8");
4725
4892
  return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
4726
4893
  } catch {
4727
4894
  return false;
@@ -4730,14 +4897,14 @@ function check(opts) {
4730
4897
  const viewportFitCover = stylesheets.some((f) => {
4731
4898
  if (!isStyleBearing(f)) return false;
4732
4899
  try {
4733
- return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync19(join24(opts.projectRoot, f), "utf8"));
4900
+ return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync21(join25(opts.projectRoot, f), "utf8"));
4734
4901
  } catch {
4735
4902
  return false;
4736
4903
  }
4737
4904
  });
4738
4905
  const projectMenuToggle = stylesheets.some((f) => {
4739
4906
  try {
4740
- return hasMenuToggle(readFileSync19(join24(opts.projectRoot, f), "utf8"));
4907
+ return hasMenuToggle(readFileSync21(join25(opts.projectRoot, f), "utf8"));
4741
4908
  } catch {
4742
4909
  return false;
4743
4910
  }
@@ -4745,7 +4912,7 @@ function check(opts) {
4745
4912
  const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
4746
4913
  const withStyles = files.filter((f) => {
4747
4914
  try {
4748
- const src = readFileSync19(join24(opts.projectRoot, f), "utf8");
4915
+ const src = readFileSync21(join25(opts.projectRoot, f), "utf8");
4749
4916
  return hasExtension(f, CSS_EXTENSIONS) ? src.trim().length > 0 : maskNonStyleRegions(src, f).trim().length > 0;
4750
4917
  } catch {
4751
4918
  return false;
@@ -4778,7 +4945,7 @@ function check(opts) {
4778
4945
  // told to add a token @import it has nowhere to put.
4779
4946
  cssFiles.length > 0 && cssFiles.every((f) => {
4780
4947
  try {
4781
- return !participatesInTokenLayer(readFileSync19(join24(opts.projectRoot, f), "utf8"), tokens);
4948
+ return !participatesInTokenLayer(readFileSync21(join25(opts.projectRoot, f), "utf8"), tokens);
4782
4949
  } catch {
4783
4950
  return true;
4784
4951
  }
@@ -4798,20 +4965,22 @@ function check(opts) {
4798
4965
  withStyles,
4799
4966
  exempt,
4800
4967
  exemptPatterns: byPattern,
4968
+ exemptIgnored,
4801
4969
  scope: selection.mode,
4802
4970
  modeUnwired: modeWiringProblems(opts.projectRoot),
4971
+ agentFiles: agentFileProblems(opts.projectRoot, opts.homeDir || void 0),
4803
4972
  waived
4804
4973
  });
4805
4974
  return { findings, report: report2, hasError, waived };
4806
4975
  }
4807
4976
 
4808
4977
  // src/commands/init.ts
4809
- import { existsSync as existsSync16, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync23, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
4810
- import { dirname as dirname7, isAbsolute, join as join28, resolve as resolve2, sep as sep3 } from "path";
4978
+ import { existsSync as existsSync17, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync25, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
4979
+ import { dirname as dirname7, isAbsolute, join as join29, resolve as resolve2, sep as sep3 } from "path";
4811
4980
 
4812
4981
  // src/init/detect.ts
4813
- import { readFileSync as readFileSync20 } from "fs";
4814
- import { join as join25 } from "path";
4982
+ import { readFileSync as readFileSync22 } from "fs";
4983
+ import { join as join26 } from "path";
4815
4984
  var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
4816
4985
  var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
4817
4986
  var TAILWIND_V4_THEME_RE = /@theme\b/;
@@ -4828,7 +4997,7 @@ var FRAMEWORK_DEPS = [
4828
4997
  function detectFramework(projectRoot) {
4829
4998
  let pkg;
4830
4999
  try {
4831
- pkg = JSON.parse(readFileSync20(join25(projectRoot, "package.json"), "utf8"));
5000
+ pkg = JSON.parse(readFileSync22(join26(projectRoot, "package.json"), "utf8"));
4832
5001
  } catch {
4833
5002
  return void 0;
4834
5003
  }
@@ -4846,7 +5015,7 @@ function detect(projectRoot) {
4846
5015
  for (const f of cssFiles) {
4847
5016
  let source;
4848
5017
  try {
4849
- source = readFileSync20(join25(projectRoot, f), "utf8");
5018
+ source = readFileSync22(join26(projectRoot, f), "utf8");
4850
5019
  } catch {
4851
5020
  continue;
4852
5021
  }
@@ -4881,8 +5050,8 @@ function isIgnored(projectRoot, relPath) {
4881
5050
  }
4882
5051
 
4883
5052
  // src/init/derive.ts
4884
- import { readFileSync as readFileSync21 } from "fs";
4885
- import { join as join26 } from "path";
5053
+ import { readFileSync as readFileSync23 } from "fs";
5054
+ import { join as join27 } from "path";
4886
5055
  var DEFAULT_PROPOSAL = {
4887
5056
  h: 264,
4888
5057
  s: 0,
@@ -4915,7 +5084,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
4915
5084
  for (const file of cssFiles) {
4916
5085
  let source;
4917
5086
  try {
4918
- source = readFileSync21(join26(projectRoot, file), "utf8");
5087
+ source = readFileSync23(join27(projectRoot, file), "utf8");
4919
5088
  } catch {
4920
5089
  continue;
4921
5090
  }
@@ -4964,7 +5133,7 @@ function balancedBraceBody(source, openIdx) {
4964
5133
  function fromTailwindConfig(projectRoot, tailwindConfigFile) {
4965
5134
  let source;
4966
5135
  try {
4967
- source = readFileSync21(join26(projectRoot, tailwindConfigFile), "utf8");
5136
+ source = readFileSync23(join27(projectRoot, tailwindConfigFile), "utf8");
4968
5137
  } catch {
4969
5138
  return null;
4970
5139
  }
@@ -5020,7 +5189,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
5020
5189
  for (const file of cssFiles) {
5021
5190
  let source;
5022
5191
  try {
5023
- source = readFileSync21(join26(projectRoot, file), "utf8");
5192
+ source = readFileSync23(join27(projectRoot, file), "utf8");
5024
5193
  } catch {
5025
5194
  continue;
5026
5195
  }
@@ -5180,8 +5349,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
5180
5349
  }
5181
5350
 
5182
5351
  // src/init/project-name.ts
5183
- import { readFileSync as readFileSync22 } from "fs";
5184
- import { basename, join as join27 } from "path";
5352
+ import { readFileSync as readFileSync24 } from "fs";
5353
+ import { basename, join as join28 } from "path";
5185
5354
  function slugify(name) {
5186
5355
  const withoutScope = name.replace(/^@[^/]+\//, "");
5187
5356
  const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -5189,7 +5358,7 @@ function slugify(name) {
5189
5358
  }
5190
5359
  function deriveProjectSlug(projectRoot) {
5191
5360
  try {
5192
- const pkg = JSON.parse(readFileSync22(join27(projectRoot, "package.json"), "utf8"));
5361
+ const pkg = JSON.parse(readFileSync24(join28(projectRoot, "package.json"), "utf8"));
5193
5362
  if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
5194
5363
  } catch {
5195
5364
  }
@@ -5330,7 +5499,7 @@ function parseSurfaces(answer, fallback) {
5330
5499
  return parsed.length > 0 ? parsed : fallback;
5331
5500
  }
5332
5501
  function fileState(projectRoot, absPath, relPath, initManifest) {
5333
- const existsOnDisk = existsSync16(absPath);
5502
+ const existsOnDisk = existsSync17(absPath);
5334
5503
  const tracked = !!initManifest && relPath in initManifest.files;
5335
5504
  const modified = tracked && isInitFileModified(projectRoot, relPath, initManifest);
5336
5505
  return { existsOnDisk, tracked, modified };
@@ -5341,11 +5510,11 @@ function isValidSurfaceArray(v) {
5341
5510
  );
5342
5511
  }
5343
5512
  function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
5344
- if (!existsSync16(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
5513
+ if (!existsSync17(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
5345
5514
  try {
5346
- const parsed = JSON.parse(readFileSync23(configAbsPath, "utf8"));
5515
+ const parsed = JSON.parse(readFileSync25(configAbsPath, "utf8"));
5347
5516
  const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
5348
- const brandResolves = brandCandidate ? existsSync16(join28(projectRoot, ...brandCandidate.split("/"))) : false;
5517
+ const brandResolves = brandCandidate ? existsSync17(join29(projectRoot, ...brandCandidate.split("/"))) : false;
5349
5518
  return {
5350
5519
  brand: brandResolves ? brandCandidate : fallbackBrand,
5351
5520
  surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
@@ -5355,10 +5524,10 @@ function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallback
5355
5524
  }
5356
5525
  }
5357
5526
  function configuredBrandPath(projectRoot, configAbsPath) {
5358
- if (!existsSync16(configAbsPath)) return { path: null };
5527
+ if (!existsSync17(configAbsPath)) return { path: null };
5359
5528
  let declared;
5360
5529
  try {
5361
- declared = JSON.parse(readFileSync23(configAbsPath, "utf8")).brand;
5530
+ declared = JSON.parse(readFileSync25(configAbsPath, "utf8")).brand;
5362
5531
  } catch {
5363
5532
  return { path: null };
5364
5533
  }
@@ -5386,9 +5555,9 @@ function defaultTokenDir(detection) {
5386
5555
  return ["jig"];
5387
5556
  }
5388
5557
  function readConfiguredSurfaces(configAbsPath) {
5389
- if (!existsSync16(configAbsPath)) return null;
5558
+ if (!existsSync17(configAbsPath)) return null;
5390
5559
  try {
5391
- const parsed = JSON.parse(readFileSync23(configAbsPath, "utf8"));
5560
+ const parsed = JSON.parse(readFileSync25(configAbsPath, "utf8"));
5392
5561
  return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
5393
5562
  } catch {
5394
5563
  return null;
@@ -5398,8 +5567,8 @@ function isCssModule(path) {
5398
5567
  return /\.module\.(css|scss)$/i.test(path);
5399
5568
  }
5400
5569
  function legacyBrandFile(projectRoot) {
5401
- const dir = join28(projectRoot, ".jig", "tokens");
5402
- if (!existsSync16(dir)) return void 0;
5570
+ const dir = join29(projectRoot, ".jig", "tokens");
5571
+ if (!existsSync17(dir)) return void 0;
5403
5572
  const found = readdirSync10(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
5404
5573
  return found ? relKey(".jig", "tokens", found) : void 0;
5405
5574
  }
@@ -5430,7 +5599,7 @@ var LEGACY_PAIR_RE = new RegExp(
5430
5599
  `@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
5431
5600
  );
5432
5601
  function wireBarrel(absPath, barrelImport) {
5433
- const content = existsSync16(absPath) ? readFileSync23(absPath, "utf8") : "";
5602
+ const content = existsSync17(absPath) ? readFileSync25(absPath, "utf8") : "";
5434
5603
  const line = `@import "${barrelImport}";`;
5435
5604
  if (content.includes(line)) return { status: "already-present" };
5436
5605
  const legacy = LEGACY_PAIR_RE.exec(content);
@@ -5525,7 +5694,7 @@ async function init(opts) {
5525
5694
  else if (critiqueAnswer) log(` Could not read '${critiqueAnswer}' \u2014 leaving it page by page; each spec will ask.`);
5526
5695
  }
5527
5696
  if (opts.yes) {
5528
- const declared = readConfiguredSurfaces(join28(opts.projectRoot, "jig.config.json"));
5697
+ const declared = readConfiguredSurfaces(join29(opts.projectRoot, "jig.config.json"));
5529
5698
  const inEffect = declared ?? surfaces;
5530
5699
  const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
5531
5700
  log(
@@ -5544,7 +5713,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
5544
5713
  const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
5545
5714
  const projectSlug = deriveProjectSlug(opts.projectRoot);
5546
5715
  const configRelPath = "jig.config.json";
5547
- const configAbsPath = join28(opts.projectRoot, configRelPath);
5716
+ const configAbsPath = join29(opts.projectRoot, configRelPath);
5548
5717
  const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
5549
5718
  if (configuredBrand.refused) {
5550
5719
  log(
@@ -5578,7 +5747,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
5578
5747
  }
5579
5748
  }
5580
5749
  const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
5581
- const brandAbsPath = join28(opts.projectRoot, ...brandRelPath.split("/"));
5750
+ const brandAbsPath = join29(opts.projectRoot, ...brandRelPath.split("/"));
5582
5751
  const tokensRelDir = brandRelPath.split("/").slice(0, -1);
5583
5752
  log(
5584
5753
  `Token layer: ${tokensRelDir.join("/") || "."}/ \u2014 ` + (configuredBrand.path ? "from jig.config.json." : priorBrand ? "the layout this project already had. Set `brand` in jig.config.json to move it." : findWireTarget(detection) ? "beside the stylesheet being wired. Set `brand` in jig.config.json to put it elsewhere." : "no stylesheet found to follow, so the project root. Set `brand` in jig.config.json to move it.")
@@ -5611,7 +5780,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5611
5780
  if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
5612
5781
  }
5613
5782
  if (brandAction === "written") {
5614
- const defaultCssRaw = readFileSync23(join28(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
5783
+ const defaultCssRaw = readFileSync25(join29(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
5615
5784
  const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
5616
5785
  mkdirSync6(dirname7(brandAbsPath), { recursive: true });
5617
5786
  writeFileSync4(brandAbsPath, content, "utf8");
@@ -5619,7 +5788,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5619
5788
  }
5620
5789
  if (relocateTo && configAction !== "written") {
5621
5790
  try {
5622
- const existing = JSON.parse(readFileSync23(configAbsPath, "utf8"));
5791
+ const existing = JSON.parse(readFileSync25(configAbsPath, "utf8"));
5623
5792
  const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
5624
5793
  `;
5625
5794
  writeFileSync4(configAbsPath, content, "utf8");
@@ -5638,12 +5807,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5638
5807
  } else if (configAction === "merged") {
5639
5808
  let existing = {};
5640
5809
  try {
5641
- existing = JSON.parse(readFileSync23(configAbsPath, "utf8"));
5810
+ existing = JSON.parse(readFileSync25(configAbsPath, "utf8"));
5642
5811
  } catch {
5643
5812
  existing = {};
5644
5813
  }
5645
5814
  const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
5646
- const brandStillResolves = existingBrand ? existsSync16(join28(opts.projectRoot, ...existingBrand.split("/"))) : false;
5815
+ const brandStillResolves = existingBrand ? existsSync17(join29(opts.projectRoot, ...existingBrand.split("/"))) : false;
5647
5816
  const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
5648
5817
  const merged = {
5649
5818
  ...existing,
@@ -5660,12 +5829,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5660
5829
  }
5661
5830
  const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
5662
5831
  const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
5663
- const wiringBrandAbsPath = join28(opts.projectRoot, ...effectiveConfig.brand.split("/"));
5832
+ const wiringBrandAbsPath = join29(opts.projectRoot, ...effectiveConfig.brand.split("/"));
5664
5833
  const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
5665
5834
  const modeAbsPaths = {};
5666
5835
  for (const mode of declaredModes) {
5667
5836
  const modeFileName = `mode.${mode}.css`;
5668
- const modeAbsPath2 = join28(opts.projectRoot, ...tokensRelDir, modeFileName);
5837
+ const modeAbsPath2 = join29(opts.projectRoot, ...tokensRelDir, modeFileName);
5669
5838
  modeAbsPaths[mode] = modeAbsPath2;
5670
5839
  const modeRelPath = relKey(...tokensRelDir, modeFileName);
5671
5840
  const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
@@ -5673,7 +5842,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5673
5842
  if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
5674
5843
  else if (modeState.modified) modeAction = "skipped-edited";
5675
5844
  if (modeAction === "written") {
5676
- const modeSourceRaw = readFileSync23(join28(opts.packageRoot, "tokens", modeFileName), "utf8");
5845
+ const modeSourceRaw = readFileSync25(join29(opts.packageRoot, "tokens", modeFileName), "utf8");
5677
5846
  const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
5678
5847
  mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
5679
5848
  writeFileSync4(modeAbsPath2, content, "utf8");
@@ -5688,7 +5857,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5688
5857
  const barrelFor = (mode) => multiMode ? `theme.${mode}.css` : "theme.css";
5689
5858
  for (const mode of declaredModes) {
5690
5859
  const rel = relKey(...tokensRelDir, barrelFor(mode));
5691
- const abs = join28(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
5860
+ const abs = join29(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
5692
5861
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5693
5862
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
5694
5863
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
@@ -5701,8 +5870,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5701
5870
  }
5702
5871
  if (multiMode) {
5703
5872
  const rel = relKey(...tokensRelDir, "theme.css");
5704
- const abs = join28(opts.projectRoot, ...tokensRelDir, "theme.css");
5705
- if (existsSync16(abs)) {
5873
+ const abs = join29(opts.projectRoot, ...tokensRelDir, "theme.css");
5874
+ if (existsSync17(abs)) {
5706
5875
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5707
5876
  if (state.tracked && !state.modified) {
5708
5877
  rmSync2(abs);
@@ -5717,7 +5886,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5717
5886
  const declared = declaredTokenNames(
5718
5887
  [brandAbsPath, modeAbsPath].map((f) => {
5719
5888
  try {
5720
- return readFileSync23(f, "utf8");
5889
+ return readFileSync25(f, "utf8");
5721
5890
  } catch {
5722
5891
  return "";
5723
5892
  }
@@ -5742,7 +5911,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5742
5911
  log(` This changes how components are written. Both styles are correct.`);
5743
5912
  const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
5744
5913
  if (answer === "y" || answer === "yes") {
5745
- const abs = join28(opts.projectRoot, ...tokensRelDir, "utilities.css");
5914
+ const abs = join29(opts.projectRoot, ...tokensRelDir, "utilities.css");
5746
5915
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5747
5916
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
5748
5917
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
@@ -5753,8 +5922,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5753
5922
  files[rel] = checksum(content);
5754
5923
  const target = findWireTarget(detection);
5755
5924
  const importPath = target ? relativeImportPath(
5756
- dirname7(join28(opts.projectRoot, target)),
5757
- join28(opts.projectRoot, ...tokensRelDir, "utilities.css")
5925
+ dirname7(join29(opts.projectRoot, target)),
5926
+ join29(opts.projectRoot, ...tokensRelDir, "utilities.css")
5758
5927
  ) : `./${rel}`;
5759
5928
  log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
5760
5929
  log(` @import "tailwindcss";`);
@@ -5773,11 +5942,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5773
5942
  writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
5774
5943
  if (relocateTo && currentBrand) {
5775
5944
  const oldDir = currentBrand.split("/").slice(0, -1);
5776
- const oldAbsDir = join28(opts.projectRoot, ...oldDir);
5777
- if (existsSync16(oldAbsDir)) {
5945
+ const oldAbsDir = join29(opts.projectRoot, ...oldDir);
5946
+ if (existsSync17(oldAbsDir)) {
5778
5947
  for (const name of readdirSync10(oldAbsDir).filter((f) => f.endsWith(".css"))) {
5779
5948
  const rel = relKey(...oldDir, name);
5780
- const abs = join28(oldAbsDir, name);
5949
+ const abs = join29(oldAbsDir, name);
5781
5950
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5782
5951
  if (!state.tracked || state.modified) {
5783
5952
  log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
@@ -5791,11 +5960,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5791
5960
  const wireTarget = findWireTarget(detection);
5792
5961
  let wiring;
5793
5962
  if (multiMode) {
5794
- const bare2 = join28(opts.projectRoot, ...tokensRelDir, "theme.css");
5963
+ const bare2 = join29(opts.projectRoot, ...tokensRelDir, "theme.css");
5795
5964
  for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
5796
- const abs = join28(opts.projectRoot, file);
5965
+ const abs = join29(opts.projectRoot, file);
5797
5966
  try {
5798
- const before = readFileSync23(abs, "utf8");
5967
+ const before = readFileSync25(abs, "utf8");
5799
5968
  const after = before.split("\n").filter((l) => {
5800
5969
  const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
5801
5970
  return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname7(abs), m[1]) === bare2);
@@ -5817,21 +5986,21 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
5817
5986
  snippet: effectiveConfig.surfaces.map((s) => `${s.match} \u2192 ${relKey(...tokensRelDir, barrelFor(s.mode))}`).join("\n")
5818
5987
  };
5819
5988
  } else if (wireTarget) {
5820
- const targetAbsDir = dirname7(join28(opts.projectRoot, wireTarget));
5989
+ const targetAbsDir = dirname7(join29(opts.projectRoot, wireTarget));
5821
5990
  const barrelImport = relativeImportPath(
5822
5991
  targetAbsDir,
5823
- join28(opts.projectRoot, ...tokensRelDir, "theme.css")
5992
+ join29(opts.projectRoot, ...tokensRelDir, "theme.css")
5824
5993
  );
5825
5994
  const snippet = `@import "${barrelImport}";`;
5826
5995
  try {
5827
5996
  if (relocateTo && currentBrand) {
5828
- const abs = join28(opts.projectRoot, wireTarget);
5997
+ const abs = join29(opts.projectRoot, wireTarget);
5829
5998
  const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
5830
- const before = existsSync16(abs) ? readFileSync23(abs, "utf8") : "";
5999
+ const before = existsSync17(abs) ? readFileSync25(abs, "utf8") : "";
5831
6000
  const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
5832
6001
  if (after !== before) writeFileSync4(abs, after, "utf8");
5833
6002
  }
5834
- const outcome = wireBarrel(join28(opts.projectRoot, wireTarget), barrelImport);
6003
+ const outcome = wireBarrel(join29(opts.projectRoot, wireTarget), barrelImport);
5835
6004
  wiring = { target: wireTarget, status: outcome.status, snippet };
5836
6005
  const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
5837
6006
  log(`
@@ -5848,7 +6017,7 @@ Could not write to ${wireTarget}: ${err.message}`);
5848
6017
  } else {
5849
6018
  const barrelImport = relativeImportPath(
5850
6019
  opts.projectRoot,
5851
- join28(opts.projectRoot, ...tokensRelDir, "theme.css")
6020
+ join29(opts.projectRoot, ...tokensRelDir, "theme.css")
5852
6021
  );
5853
6022
  const snippet = `@import "${barrelImport}";`;
5854
6023
  wiring = { target: null, status: "print-only", snippet };
@@ -5857,7 +6026,7 @@ Could not write to ${wireTarget}: ${err.message}`);
5857
6026
  if (pasteTargets.length > 0) {
5858
6027
  log("Paste the block for whichever of these is your global stylesheet:");
5859
6028
  for (const target of pasteTargets) {
5860
- const dir = dirname7(join28(opts.projectRoot, target));
6029
+ const dir = dirname7(join29(opts.projectRoot, target));
5861
6030
  const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
5862
6031
  @import "${relativeImportPath(dir, modeAbsPath)}";`;
5863
6032
  log(`
@@ -5883,6 +6052,9 @@ Could not write to ${wireTarget}: ${err.message}`);
5883
6052
  "\n WARNING: .jig/ is gitignored. It holds state.json \u2014 which `update` reads to leave your edited files alone \u2014 and the specs, mockups and critique verdicts this project agreed. Ignored, update cannot tell your edits from its own writes, and every spec is invisible to the next agent. Commit it."
5884
6053
  );
5885
6054
  }
6055
+ const agentFiles = ensureAgentFiles(opts.projectRoot, { claude: claudeInstalled(opts.projectRoot, opts.homeDir) });
6056
+ if (agentFiles.length) log(`
6057
+ Wrote Jig's instructions for your agent into ${agentFiles.join(" and ")}; your own text in them is untouched.`);
5886
6058
  const baselineResult = check({ projectRoot: opts.projectRoot, homeDir: opts.homeDir, version: opts.version, all: true, ci: false });
5887
6059
  log("\nBaseline:");
5888
6060
  log(baselineResult.report);
@@ -5905,28 +6077,28 @@ Could not write to ${wireTarget}: ${err.message}`);
5905
6077
 
5906
6078
  // src/commands/verdicts.ts
5907
6079
  import { execFileSync as execFileSync3 } from "child_process";
5908
- import { existsSync as existsSync23, readFileSync as readFileSync30 } from "fs";
5909
- import { join as join37, relative as relative4, resolve as resolve5 } from "path";
6080
+ import { existsSync as existsSync24, readFileSync as readFileSync32 } from "fs";
6081
+ import { join as join38, relative as relative4, resolve as resolve5, sep as sep6 } from "path";
5910
6082
 
5911
6083
  // src/rules/citations.ts
5912
- import { join as join29 } from "path";
6084
+ import { join as join30 } from "path";
5913
6085
  function citableIds(packageRoot2) {
5914
- const rulesDir = join29(packageRoot2, "rules");
6086
+ const rulesDir = join30(packageRoot2, "rules");
5915
6087
  const ids = [
5916
- ...loadRules(rulesDir, join29(packageRoot2, "rules.index.json")).map((r) => r.id),
6088
+ ...loadRules(rulesDir, join30(packageRoot2, "rules.index.json")).map((r) => r.id),
5917
6089
  ...loadSpecs(rulesDir).map((s) => s.id)
5918
6090
  ];
5919
6091
  return [...new Set(ids)].sort();
5920
6092
  }
5921
6093
 
5922
6094
  // src/probe/check.ts
5923
- import { existsSync as existsSync20, readdirSync as readdirSync13, readFileSync as readFileSync27 } from "fs";
5924
- import { join as join34 } from "path";
6095
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync29 } from "fs";
6096
+ import { join as join35 } from "path";
5925
6097
 
5926
6098
  // src/probe/save.ts
5927
- import { existsSync as existsSync19, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync26, writeFileSync as writeFileSync5 } from "fs";
6099
+ import { existsSync as existsSync20, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync28, writeFileSync as writeFileSync5 } from "fs";
5928
6100
  import { fileURLToPath as fileURLToPath2 } from "url";
5929
- import { join as join33, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
6101
+ import { join as join34, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
5930
6102
 
5931
6103
  // src/probe/script.ts
5932
6104
  var PROBE_VERSION = 9;
@@ -6158,24 +6330,28 @@ var PROBE_SCRIPT = `(async () => {
6158
6330
 
6159
6331
  // src/probe/browser.ts
6160
6332
  import { spawn } from "child_process";
6161
- import { existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync3 } from "fs";
6162
- import { homedir, tmpdir } from "os";
6163
- import { join as join30 } from "path";
6333
+ import { existsSync as existsSync18, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync3 } from "fs";
6334
+ import { homedir as homedir2, tmpdir } from "os";
6335
+ import { join as join31 } from "path";
6164
6336
  var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
6165
- var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser"];
6337
+ var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "chrome", "msedge"];
6166
6338
  var CHROME_APPS = [
6167
6339
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
6168
6340
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
6169
6341
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
6170
6342
  "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
6171
- "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
6343
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
6344
+ // Every Windows machine has Edge; Chrome is often installed per user.
6345
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
6346
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
6347
+ ...process.env.LOCALAPPDATA ? [join31(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
6172
6348
  ];
6173
6349
  function cachedBrowsers() {
6174
6350
  const roots = [
6175
- join30(homedir(), ".cache", "ms-playwright"),
6176
- join30(homedir(), "Library", "Caches", "ms-playwright"),
6177
- join30(homedir(), "AppData", "Local", "ms-playwright"),
6178
- join30(homedir(), ".cache", "puppeteer", "chrome")
6351
+ join31(homedir2(), ".cache", "ms-playwright"),
6352
+ join31(homedir2(), "Library", "Caches", "ms-playwright"),
6353
+ join31(homedir2(), "AppData", "Local", "ms-playwright"),
6354
+ join31(homedir2(), ".cache", "puppeteer", "chrome")
6179
6355
  ];
6180
6356
  const found = [];
6181
6357
  for (const root of roots) {
@@ -6195,8 +6371,8 @@ function cachedBrowsers() {
6195
6371
  ["chrome-win", "chrome.exe"],
6196
6372
  ["chrome-win64", "chrome.exe"]
6197
6373
  ]) {
6198
- const candidate = join30(root, entry, ...rel);
6199
- if (existsSync17(candidate)) found.push(candidate);
6374
+ const candidate = join31(root, entry, ...rel);
6375
+ if (existsSync18(candidate)) found.push(candidate);
6200
6376
  }
6201
6377
  }
6202
6378
  }
@@ -6205,21 +6381,25 @@ function cachedBrowsers() {
6205
6381
  function findChrome() {
6206
6382
  for (const key of CHROME_ENV) {
6207
6383
  const value = process.env[key];
6208
- if (value && existsSync17(value)) return value;
6384
+ if (value && existsSync18(value)) return value;
6209
6385
  }
6210
6386
  for (const command of CHROME_COMMANDS) {
6211
6387
  const resolved = which(command);
6212
6388
  if (resolved) return resolved;
6213
6389
  }
6214
- for (const app of CHROME_APPS) if (existsSync17(app)) return app;
6390
+ for (const app of CHROME_APPS) if (existsSync18(app)) return app;
6215
6391
  return cachedBrowsers()[0];
6216
6392
  }
6217
6393
  function which(command) {
6218
- const dirs = (process.env.PATH ?? "").split(process.platform === "win32" ? ";" : ":");
6394
+ const windows = process.platform === "win32";
6395
+ const dirs = (process.env.PATH ?? "").split(windows ? ";" : ":");
6396
+ const exts = windows ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
6219
6397
  for (const dir of dirs) {
6220
6398
  if (!dir) continue;
6221
- const candidate = join30(dir, command);
6222
- if (existsSync17(candidate)) return candidate;
6399
+ for (const ext of exts) {
6400
+ const candidate = join31(dir, command + ext);
6401
+ if (existsSync18(candidate)) return candidate;
6402
+ }
6223
6403
  }
6224
6404
  return void 0;
6225
6405
  }
@@ -6230,7 +6410,7 @@ async function runProbe(opts) {
6230
6410
  "No browser found to run the probe. Install one, or set JIG_CHROME to a Chrome, Chromium or Edge binary. A project with Playwright or Puppeteer already has one, and Jig will find it."
6231
6411
  );
6232
6412
  }
6233
- const profile = mkdtempSync(join30(tmpdir(), "jig-probe-"));
6413
+ const profile = mkdtempSync(join31(tmpdir(), "jig-probe-"));
6234
6414
  const child = spawn(chrome, [
6235
6415
  "--headless=new",
6236
6416
  "--remote-debugging-port=0",
@@ -6274,6 +6454,9 @@ async function runProbe(opts) {
6274
6454
  }
6275
6455
  }
6276
6456
  async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
6457
+ if (typeof WebSocket === "undefined") {
6458
+ throw new Error(`rendering a page needs Node 22 or later (this is Node ${process.versions.node}). Upgrade Node, then run it again.`);
6459
+ }
6277
6460
  const socket = new WebSocket(endpoint);
6278
6461
  let nextId = 1;
6279
6462
  const pending = /* @__PURE__ */ new Map();
@@ -6331,8 +6514,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
6331
6514
 
6332
6515
  // src/probe/serve.ts
6333
6516
  import { createServer } from "http";
6334
- import { existsSync as existsSync18, readFileSync as readFileSync24, statSync as statSync3 } from "fs";
6335
- import { extname, join as join31, resolve as resolve3, sep as sep4 } from "path";
6517
+ import { existsSync as existsSync19, readFileSync as readFileSync26, statSync as statSync3 } from "fs";
6518
+ import { extname, join as join32, resolve as resolve3, sep as sep4 } from "path";
6336
6519
  var TYPES = {
6337
6520
  ".html": "text/html; charset=utf-8",
6338
6521
  ".htm": "text/html; charset=utf-8",
@@ -6359,19 +6542,19 @@ async function serveDirectory(dir) {
6359
6542
  const root = resolve3(dir);
6360
6543
  const server = createServer((req, res) => {
6361
6544
  let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
6362
- let file = resolve3(join31(root, path));
6545
+ let file = resolve3(join32(root, path));
6363
6546
  if (file !== root && !file.startsWith(root + sep4)) {
6364
6547
  res.writeHead(403).end();
6365
6548
  return;
6366
6549
  }
6367
- if (existsSync18(file) && statSync3(file).isDirectory()) file = join31(file, "index.html");
6368
- else if (!existsSync18(file) && existsSync18(`${file}.html`)) file = `${file}.html`;
6369
- if (!existsSync18(file)) {
6550
+ if (existsSync19(file) && statSync3(file).isDirectory()) file = join32(file, "index.html");
6551
+ else if (!existsSync19(file) && existsSync19(`${file}.html`)) file = `${file}.html`;
6552
+ if (!existsSync19(file)) {
6370
6553
  res.writeHead(404).end();
6371
6554
  return;
6372
6555
  }
6373
6556
  res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
6374
- res.end(readFileSync24(file));
6557
+ res.end(readFileSync26(file));
6375
6558
  });
6376
6559
  await new Promise((done) => server.listen(0, "127.0.0.1", done));
6377
6560
  const address = server.address();
@@ -6383,8 +6566,8 @@ async function serveDirectory(dir) {
6383
6566
  }
6384
6567
 
6385
6568
  // src/probe/switches.ts
6386
- import { readFileSync as readFileSync25 } from "fs";
6387
- import { join as join32 } from "path";
6569
+ import { readFileSync as readFileSync27 } from "fs";
6570
+ import { join as join33 } from "path";
6388
6571
  var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
6389
6572
  function declaredSwitches(projectRoot) {
6390
6573
  const found = /* @__PURE__ */ new Map();
@@ -6392,7 +6575,7 @@ function declaredSwitches(projectRoot) {
6392
6575
  if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
6393
6576
  let text;
6394
6577
  try {
6395
- text = readFileSync25(join32(projectRoot, file), "utf8");
6578
+ text = readFileSync27(join33(projectRoot, file), "utf8");
6396
6579
  } catch {
6397
6580
  continue;
6398
6581
  }
@@ -6427,18 +6610,18 @@ function saveProbe(opts) {
6427
6610
  }
6428
6611
  probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
6429
6612
  if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
6430
- probe.pageChecksum = checksum(readFileSync26(page, "utf8"));
6613
+ probe.pageChecksum = checksum(readFileSync28(page, "utf8"));
6431
6614
  probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
6432
- const dir = join33(opts.projectRoot, ".jig", "critique", opts.surface);
6615
+ const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
6433
6616
  mkdirSync7(dir, { recursive: true });
6434
- const path = join33(dir, `probe-${width}.json`);
6617
+ const path = join34(dir, `probe-${width}.json`);
6435
6618
  writeFileSync5(path, JSON.stringify(probe), "utf8");
6436
6619
  return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
6437
6620
  }
6438
6621
  function projectFile(projectRoot, path) {
6439
6622
  const abs = resolve4(projectRoot, path);
6440
6623
  if (abs !== resolve4(projectRoot) && !abs.startsWith(resolve4(projectRoot) + sep5)) return void 0;
6441
- return existsSync19(abs) ? abs : void 0;
6624
+ return existsSync20(abs) ? abs : void 0;
6442
6625
  }
6443
6626
  function pageFile(projectRoot, url) {
6444
6627
  if (!url.startsWith("file://")) return void 0;
@@ -6450,12 +6633,12 @@ function pageFile(projectRoot, url) {
6450
6633
  }
6451
6634
  const abs = resolve4(path);
6452
6635
  if (!abs.startsWith(resolve4(projectRoot))) return void 0;
6453
- return existsSync19(abs) ? abs : void 0;
6636
+ return existsSync20(abs) ? abs : void 0;
6454
6637
  }
6455
6638
  var PROBE_WIDTHS = [360, 768, 1280, 1600];
6456
6639
  async function runAndSaveProbes(opts) {
6457
6640
  const abs = resolve4(opts.projectRoot, opts.page);
6458
- if (!existsSync19(abs)) throw new Error(`${opts.page} does not exist, so there is nothing to render.`);
6641
+ if (!existsSync20(abs)) throw new Error(`${opts.page} does not exist, so there is nothing to render.`);
6459
6642
  const saved = [];
6460
6643
  const widths = opts.widths ?? probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot));
6461
6644
  await withPageUrl(opts.projectRoot, abs, opts.serve, async (url) => {
@@ -6469,7 +6652,7 @@ async function runAndSaveProbes(opts) {
6469
6652
  async function withPageUrl(projectRoot, abs, serve, use) {
6470
6653
  if (!serve) return use(`file://${abs}`);
6471
6654
  const root = resolve4(projectRoot, serve);
6472
- if (!existsSync19(root)) throw new Error(`${serve} does not exist, so there is nothing to serve.`);
6655
+ if (!existsSync20(root)) throw new Error(`${serve} does not exist, so there is nothing to serve.`);
6473
6656
  if (!abs.startsWith(root + sep5)) throw new Error(`The page is not inside ${serve}, so serving ${serve} cannot show it.`);
6474
6657
  const server = await serveDirectory(root);
6475
6658
  try {
@@ -6480,31 +6663,31 @@ async function withPageUrl(projectRoot, abs, serve, use) {
6480
6663
  }
6481
6664
  }
6482
6665
  function recordedPage(projectRoot, surface) {
6483
- const dir = join33(projectRoot, ".jig", "critique", surface);
6484
- if (!existsSync19(dir)) return void 0;
6666
+ const dir = join34(projectRoot, ".jig", "critique", surface);
6667
+ if (!existsSync20(dir)) return void 0;
6485
6668
  for (const f of readdirSync12(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
6486
6669
  try {
6487
- const probe = JSON.parse(readFileSync26(join33(dir, f), "utf8"));
6488
- if (probe.pageFile && existsSync19(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
6670
+ const probe = JSON.parse(readFileSync28(join34(dir, f), "utf8"));
6671
+ if (probe.pageFile && existsSync20(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
6489
6672
  } catch {
6490
6673
  }
6491
6674
  }
6492
6675
  return void 0;
6493
6676
  }
6494
6677
  async function ensureProbes(opts) {
6495
- const dir = join33(opts.projectRoot, ".jig", "critique", opts.surface);
6678
+ const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
6496
6679
  const abs = resolve4(opts.projectRoot, opts.page);
6497
6680
  let current;
6498
6681
  try {
6499
- current = checksum(readFileSync26(abs, "utf8"));
6682
+ current = checksum(readFileSync28(abs, "utf8"));
6500
6683
  } catch {
6501
6684
  return { recorded: [], reason: `${opts.page} could not be read` };
6502
6685
  }
6503
- const recorded = existsSync19(dir) ? readdirSync12(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
6686
+ const recorded = existsSync20(dir) ? readdirSync12(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
6504
6687
  const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
6505
6688
  const missing = widths.filter((width) => {
6506
6689
  try {
6507
- const probe = JSON.parse(readFileSync26(join33(dir, `probe-${width}.json`), "utf8"));
6690
+ const probe = JSON.parse(readFileSync28(join34(dir, `probe-${width}.json`), "utf8"));
6508
6691
  return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
6509
6692
  } catch {
6510
6693
  return true;
@@ -6515,7 +6698,7 @@ async function ensureProbes(opts) {
6515
6698
  let serve;
6516
6699
  for (const width of widths) {
6517
6700
  try {
6518
- const probe = JSON.parse(readFileSync26(join33(dir, `probe-${width}.json`), "utf8"));
6701
+ const probe = JSON.parse(readFileSync28(join34(dir, `probe-${width}.json`), "utf8"));
6519
6702
  if (probe.serveRoot) {
6520
6703
  serve = probe.serveRoot;
6521
6704
  break;
@@ -6531,9 +6714,9 @@ async function ensureProbes(opts) {
6531
6714
  return { recorded: missing };
6532
6715
  }
6533
6716
  function critiquedSurfaces(projectRoot) {
6534
- const root = join33(projectRoot, ".jig", "critique");
6535
- if (!existsSync19(root)) return [];
6536
- return readdirSync12(root).filter((surface) => !surface.startsWith("_") && (existsSync19(join33(root, surface, "screen.json")) || existsSync19(join33(root, surface, "code.json"))));
6717
+ const root = join34(projectRoot, ".jig", "critique");
6718
+ if (!existsSync20(root)) return [];
6719
+ return readdirSync12(root).filter((surface) => !surface.startsWith("_") && (existsSync20(join34(root, surface, "screen.json")) || existsSync20(join34(root, surface, "code.json"))));
6537
6720
  }
6538
6721
 
6539
6722
  // src/probe/check.ts
@@ -6545,7 +6728,7 @@ function stampProblem(projectRoot, file, p) {
6545
6728
  if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
6546
6729
  let current;
6547
6730
  try {
6548
- current = readFileSync27(page, "utf8");
6731
+ current = readFileSync29(page, "utf8");
6549
6732
  } catch {
6550
6733
  return `${file} was taken on ${p.pageFile}, which no longer exists.`;
6551
6734
  }
@@ -6555,11 +6738,11 @@ function stampProblem(projectRoot, file, p) {
6555
6738
  return void 0;
6556
6739
  }
6557
6740
  function readProbes(projectRoot, dir, errors) {
6558
- if (!existsSync20(dir)) return [];
6741
+ if (!existsSync21(dir)) return [];
6559
6742
  const probes = [];
6560
6743
  for (const f of readdirSync13(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
6561
6744
  try {
6562
- const p = JSON.parse(readFileSync27(join34(dir, f), "utf8"));
6745
+ const p = JSON.parse(readFileSync29(join35(dir, f), "utf8"));
6563
6746
  if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
6564
6747
  errors.push(`${f} was taken by an older \`jig probe\` (version ${p.jigProbe}; this Jig reads version ${PROBE_VERSION}). Record it again with \`jig probe --run\`.`);
6565
6748
  continue;
@@ -6663,19 +6846,19 @@ function probeContradictions(probes, verdictOf, indexable = true) {
6663
6846
  }
6664
6847
 
6665
6848
  // src/check/decisions.ts
6666
- import { existsSync as existsSync21, readFileSync as readFileSync28 } from "fs";
6667
- import { join as join35, posix as posix3 } from "path";
6849
+ import { existsSync as existsSync22, readFileSync as readFileSync30 } from "fs";
6850
+ import { join as join36, posix as posix3 } from "path";
6668
6851
  var HEADING4 = /^(#{2,3})\s+(.+?)\s*$/gm;
6669
6852
  var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
6670
6853
  function decisionsFile(projectRoot) {
6671
6854
  const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
6672
6855
  try {
6673
- const brand = JSON.parse(readFileSync28(join35(projectRoot, "jig.config.json"), "utf8")).brand;
6856
+ const brand = JSON.parse(readFileSync30(join36(projectRoot, "jig.config.json"), "utf8")).brand;
6674
6857
  if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
6675
6858
  } catch {
6676
6859
  }
6677
6860
  for (const candidate of candidates) {
6678
- if (existsSync21(join35(projectRoot, candidate))) return candidate;
6861
+ if (existsSync22(join36(projectRoot, candidate))) return candidate;
6679
6862
  }
6680
6863
  return void 0;
6681
6864
  }
@@ -6688,7 +6871,7 @@ function decisionHeadings(projectRoot) {
6688
6871
  if (!path) return headings;
6689
6872
  let body;
6690
6873
  try {
6691
- body = readFileSync28(join35(projectRoot, path), "utf8");
6874
+ body = readText(join36(projectRoot, path));
6692
6875
  } catch {
6693
6876
  return headings;
6694
6877
  }
@@ -6746,7 +6929,7 @@ function unsourcedReasons(current, before, opts = {}) {
6746
6929
  }
6747
6930
  return problems;
6748
6931
  }
6749
- function quotesNotFrom(current, before, words) {
6932
+ function quotesNotFrom(current, before, words, source = "the owner's words in tweak.json (`change`)") {
6750
6933
  const problems = [];
6751
6934
  for (const [name, whys] of changedWhys(current, before, true)) {
6752
6935
  for (const why of whys) {
@@ -6754,7 +6937,7 @@ function quotesNotFrom(current, before, words) {
6754
6937
  if (/inferred/i.test(label)) continue;
6755
6938
  const missing = [...why.matchAll(/["“]([^"”]{3,})["”]/g)].map((m) => m[1]).filter((q) => !quoteHeld(q, words));
6756
6939
  if (missing.length) {
6757
- problems.push(`"${name}": its \`**${label}:**\` quotes "${missing[0]}", which the owner's words in tweak.json (\`change\`) do not say. Quote the owner as tweak.json records them, or put your reading under \`**Why (inferred):**\`.`);
6940
+ problems.push(`"${name}": its \`**${label}:**\` quotes "${missing[0]}", which ${source} do not say. Quote the owner as they said it, or put your reading under \`**Why (inferred):**\`.`);
6758
6941
  break;
6759
6942
  }
6760
6943
  }
@@ -6768,25 +6951,25 @@ function quoteHeld(quote, words) {
6768
6951
  }
6769
6952
 
6770
6953
  // src/check/spec-shape.ts
6771
- import { existsSync as existsSync22, readFileSync as readFileSync29, readdirSync as readdirSync14, statSync as statSync4 } from "fs";
6772
- import { join as join36 } from "path";
6954
+ import { existsSync as existsSync23, readdirSync as readdirSync14, statSync as statSync4 } from "fs";
6955
+ import { join as join37 } from "path";
6773
6956
  function newestSpec(projectRoot) {
6774
- const dir = join36(projectRoot, ".jig", "specs");
6775
- if (!existsSync22(dir)) return void 0;
6957
+ const dir = join37(projectRoot, ".jig", "specs");
6958
+ if (!existsSync23(dir)) return void 0;
6776
6959
  const files = readdirSync14(dir).filter((f) => f.endsWith(".md"));
6777
6960
  if (files.length === 0) return void 0;
6778
- const newest = files.map((f) => ({ f, at: statSync4(join36(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
6779
- return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readFileSync29(join36(dir, newest), "utf8") };
6961
+ const newest = files.map((f) => ({ f, at: statSync4(join37(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
6962
+ return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(join37(dir, newest)) };
6780
6963
  }
6781
6964
  function specFor(projectRoot, surface) {
6782
6965
  if (surface) {
6783
- const path = join36(projectRoot, ".jig", "specs", `${surface}.spec.md`);
6784
- if (existsSync22(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readFileSync29(path, "utf8") };
6966
+ const path = join37(projectRoot, ".jig", "specs", `${surface}.spec.md`);
6967
+ if (existsSync23(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
6785
6968
  }
6786
6969
  return newestSpec(projectRoot);
6787
6970
  }
6788
6971
  function specProblems(spec) {
6789
- const parts = spec.body.split(/^---\s*$/m);
6972
+ const parts = lf(spec.body).split(/^---\s*$/m);
6790
6973
  const front = parts.length >= 3 ? parts[1] : "";
6791
6974
  if (!front.trim()) {
6792
6975
  return [`${spec.path} has no frontmatter. A spec is the frontmatter \u2014 feature, surface, mode, sizes (phone, tablet, desktop), states, decisions, later, mockup, confirmed \u2014 with the reasoning below it. Rewrite it in that shape.`];
@@ -6804,7 +6987,7 @@ function specProblems(spec) {
6804
6987
  if (motion) {
6805
6988
  const inline = motion[1].replace(/\s+#.*$/, "").trim();
6806
6989
  const items = motion[2].split("\n").map((l) => l.replace(/^[ \t]+-\s*/, "").trim()).filter(Boolean);
6807
- if (!items.length && !/^none$/i.test(inline)) {
6990
+ if (!items.length && !/^none\b(\s*$|\s*[—–:,;(-])/i.test(inline)) {
6808
6991
  problems.push(`${spec.path}: \`motion:\` is neither \`none\` nor a list. List each movement as \`- <what moves>: <how>, on <trigger>; tells <what the reader learns>\`, or write \`motion: none\`.`);
6809
6992
  }
6810
6993
  const unsaid = items.filter((i) => !/\bon\b/i.test(i) || !/\btells?\b/i.test(i));
@@ -6875,6 +7058,41 @@ function specIndexableField(front) {
6875
7058
  if (value === "false" || value === "no") return false;
6876
7059
  return "unreadable";
6877
7060
  }
7061
+ var SPEC_FIELDS = [
7062
+ "feature",
7063
+ "surface",
7064
+ "route",
7065
+ "mode",
7066
+ "indexable",
7067
+ "title",
7068
+ "description",
7069
+ "sizes",
7070
+ "switches",
7071
+ "states",
7072
+ "motion",
7073
+ "decisions",
7074
+ "later",
7075
+ "mockup",
7076
+ "mockup_at",
7077
+ "deviations",
7078
+ "critique",
7079
+ "superseded_by",
7080
+ "confirmed"
7081
+ ];
7082
+ var topLevelKeys = (body) => [...(body.split(/^---\s*$/m)[1] ?? "").matchAll(/^([A-Za-z_][\w-]*)\s*:/gm)].map((m) => m[1]);
7083
+ function newFieldProblems(spec, then) {
7084
+ spec = { ...spec, body: lf(spec.body) };
7085
+ then = lf(then);
7086
+ const had = new Set(topLevelKeys(then));
7087
+ const known = new Set(SPEC_FIELDS);
7088
+ const added = [...new Set(topLevelKeys(spec.body))].filter((k) => !known.has(k) && !had.has(k));
7089
+ if (!added.length) return [];
7090
+ const near = (k) => SPEC_FIELDS.find((f) => k.includes(f) || f.startsWith(k.replace(/[-_].*$/, "")));
7091
+ return added.map((k) => {
7092
+ const hint = near(k);
7093
+ return `${spec.path}: \`${k}:\` is not a field Jig reads, so nothing checks what it says. ` + (hint ? `If it is \`${hint}:\`, write it there. ` : "") + `Otherwise put it in the prose below the front matter. Jig's fields: ${SPEC_FIELDS.join(", ")}.`;
7094
+ });
7095
+ }
6878
7096
 
6879
7097
  // src/commands/verdicts.ts
6880
7098
  var VERDICTS = ["ok", "finding", "n/a"];
@@ -6901,19 +7119,19 @@ function reasonProblems(file, judged, errors) {
6901
7119
  }
6902
7120
  }
6903
7121
  function readJson(path, errors) {
6904
- if (!existsSync23(path)) return null;
7122
+ if (!existsSync24(path)) return null;
6905
7123
  try {
6906
- return JSON.parse(readFileSync30(path, "utf8"));
7124
+ return JSON.parse(readFileSync32(path, "utf8"));
6907
7125
  } catch (e) {
6908
7126
  errors.push(`${path}: not valid JSON (${e.message})`);
6909
7127
  return {};
6910
7128
  }
6911
7129
  }
6912
7130
  function specIndexable(projectRoot, surface) {
6913
- const path = join37(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7131
+ const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
6914
7132
  let front = "";
6915
7133
  try {
6916
- front = readFileSync30(path, "utf8").split(/^---\s*$/m)[1] ?? "";
7134
+ front = readText(path).split(/^---\s*$/m)[1] ?? "";
6917
7135
  } catch {
6918
7136
  }
6919
7137
  const declared = specIndexableField(front);
@@ -6922,13 +7140,17 @@ function specIndexable(projectRoot, surface) {
6922
7140
  return mode !== "product" && mode !== "operator";
6923
7141
  }
6924
7142
  function specNeedsNav(projectRoot, surface) {
6925
- const path = join37(projectRoot, ".jig", "specs", `${surface}.spec.md`);
6926
- if (!existsSync23(path)) return false;
6927
- const front = readFileSync30(path, "utf8").split(/^---\s*$/m)[1] ?? "";
7143
+ const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7144
+ if (!existsSync24(path)) return false;
7145
+ const front = readText(path).split(/^---\s*$/m)[1] ?? "";
6928
7146
  const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
6929
7147
  const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
6930
7148
  return navField || navRegion;
6931
7149
  }
7150
+ function unreadFields(label, file, known) {
7151
+ if (!file || typeof file !== "object") return [];
7152
+ return Object.entries(file).filter(([k, v]) => !known.includes(k) && v !== null && typeof v === "object" && Object.keys(v).length > 0).map(([k]) => `${label}: \`${k}\` is not a field Jig reads, so nothing counts what it holds. A rule's verdict goes in \`verdicts\`; what no rule names, in \`differences\`; a note, in the report.`);
7153
+ }
6932
7154
  function checkArm(name, file, required, otherPass, extraAllowed, extraRequired, errors, decisionList = []) {
6933
7155
  const total = required.length + extraRequired.length;
6934
7156
  if (file === null) {
@@ -6986,6 +7208,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
6986
7208
  } else findings++;
6987
7209
  }
6988
7210
  }
7211
+ errors.push(...unreadFields(`${name}.json`, file, ["verdicts", "differences", "rendered", "artefacts"]));
6989
7212
  const missing = [...required, ...extraRequired].filter((id) => !seen.has(id));
6990
7213
  if (missing.length) {
6991
7214
  errors.push(`${name}.json: ${missing.length} of ${total} ids have no verdict: ${missing.join(", ")}. Re-run the arm; never report a short pass.`);
@@ -6996,7 +7219,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
6996
7219
  function checkDecisions(projectRoot, dir, errors) {
6997
7220
  const required = decisionNames(projectRoot);
6998
7221
  if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
6999
- const file = readJson(join37(dir, "decisions.json"), errors);
7222
+ const file = readJson(join38(dir, "decisions.json"), errors);
7000
7223
  if (!file) {
7001
7224
  errors.push(
7002
7225
  `decisions.json is missing. Every decision in DECISIONS.md is judged against the built page, one verdict each: ${required.slice(0, 4).join(", ")}${required.length > 4 ? `, and ${required.length - 4} more` : ""}. A page can satisfy every rule and still break what this project decided.`
@@ -7005,6 +7228,7 @@ function checkDecisions(projectRoot, dir, errors) {
7005
7228
  }
7006
7229
  const list = Array.isArray(file.verdicts) ? file.verdicts : [];
7007
7230
  if (!Array.isArray(file.verdicts)) errors.push('decisions.json has no "verdicts" array.');
7231
+ errors.push(...unreadFields("decisions.json", file, ["verdicts"]));
7008
7232
  const seen = /* @__PURE__ */ new Set();
7009
7233
  const reasons = [];
7010
7234
  let findings = 0;
@@ -7050,7 +7274,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
7050
7274
  if (!path) return [];
7051
7275
  const git = (args) => execFileSync3("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7052
7276
  try {
7053
- const verdictFile = relative4(projectRoot, join37(dir, "decisions.json"));
7277
+ const verdictFile = relative4(projectRoot, join38(dir, "decisions.json")).split(sep6).join("/");
7054
7278
  if (git(["status", "--porcelain", "--", verdictFile])) return [];
7055
7279
  const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
7056
7280
  if (!judgedIn) return [];
@@ -7074,7 +7298,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
7074
7298
  function verifyVerdicts(opts) {
7075
7299
  const root = opts.packageRoot ?? assetRoot();
7076
7300
  const errors = [];
7077
- const index = JSON.parse(readFileSync30(join37(root, "rules.index.json"), "utf8"));
7301
+ const index = JSON.parse(readFileSync32(join38(root, "rules.index.json"), "utf8"));
7078
7302
  const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
7079
7303
  const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
7080
7304
  const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
@@ -7083,9 +7307,9 @@ function verifyVerdicts(opts) {
7083
7307
  ...judgment.map((r) => [r.id, r.pass ?? ""])
7084
7308
  ]);
7085
7309
  const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
7086
- const dir = join37(opts.projectRoot, ".jig", "critique", opts.surface);
7087
- const screenFile = readJson(join37(dir, "screen.json"), errors);
7088
- const codeFile = readJson(join37(dir, "code.json"), errors);
7310
+ const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
7311
+ const screenFile = readJson(join38(dir, "screen.json"), errors);
7312
+ const codeFile = readJson(join38(dir, "code.json"), errors);
7089
7313
  const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
7090
7314
  const decisionList = decisionNames(opts.projectRoot);
7091
7315
  const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
@@ -7093,7 +7317,7 @@ function verifyVerdicts(opts) {
7093
7317
  let rendered = false;
7094
7318
  if (screenFile && screenFile.rendered === true) {
7095
7319
  const artefacts = Array.isArray(screenFile.artefacts) ? screenFile.artefacts.filter((a) => typeof a === "string") : [];
7096
- const absent = artefacts.filter((a) => !existsSync23(resolve5(opts.projectRoot, a)));
7320
+ const absent = artefacts.filter((a) => !existsSync24(resolve5(opts.projectRoot, a)));
7097
7321
  if (artefacts.length === 0) errors.push("screen.json says rendered: true but lists no artefacts. A render leaves a screenshot.");
7098
7322
  else if (absent.length) errors.push(`screen.json says rendered: true, but these artefacts do not exist: ${absent.join(", ")}.`);
7099
7323
  else rendered = true;
@@ -7114,7 +7338,7 @@ function verifyVerdicts(opts) {
7114
7338
  };
7115
7339
  let specFront = "";
7116
7340
  try {
7117
- specFront = readFileSync30(join37(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`), "utf8").split(/^---\s*$/m)[1] ?? "";
7341
+ specFront = readText(join38(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
7118
7342
  } catch {
7119
7343
  }
7120
7344
  if (specIndexableField(specFront) === "unreadable") {
@@ -7149,7 +7373,7 @@ function previousFindings(projectRoot, dir) {
7149
7373
  const out = /* @__PURE__ */ new Map();
7150
7374
  let text;
7151
7375
  try {
7152
- text = at ? git(["show", `${at}:./${rel}/${f}`]) : readFileSync30(join37(dir, f), "utf8");
7376
+ text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join38(dir, f));
7153
7377
  } catch {
7154
7378
  return out;
7155
7379
  }
@@ -7231,18 +7455,17 @@ function sourceChanges(git, from, to) {
7231
7455
  }
7232
7456
 
7233
7457
  // src/commands/gate.ts
7234
- import { existsSync as existsSync26, mkdirSync as mkdirSync8, readdirSync as readdirSync16, readFileSync as readFileSync35, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
7458
+ import { existsSync as existsSync27, mkdirSync as mkdirSync8, readdirSync as readdirSync16, readFileSync as readFileSync36, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
7235
7459
  import { createHash as createHash3 } from "crypto";
7236
- import { join as join42 } from "path";
7460
+ import { join as join43 } from "path";
7237
7461
 
7238
7462
  // src/check/mockup-drawing.ts
7239
- import { readFileSync as readFileSync31 } from "fs";
7240
- import { join as join38 } from "path";
7463
+ import { join as join39 } from "path";
7241
7464
  var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
7242
7465
  function mockupDrawingProblems(root, specBody, at) {
7243
7466
  let html;
7244
7467
  try {
7245
- html = readFileSync31(join38(root, at), "utf8");
7468
+ html = readText(join39(root, at));
7246
7469
  } catch {
7247
7470
  return [];
7248
7471
  }
@@ -7303,7 +7526,7 @@ function frameRegionProblems(frames, specBody, at) {
7303
7526
  function approvedDrawingProblems(root, specBody, at) {
7304
7527
  let html;
7305
7528
  try {
7306
- html = readFileSync31(join38(root, at), "utf8");
7529
+ html = readText(join39(root, at));
7307
7530
  } catch {
7308
7531
  return [];
7309
7532
  }
@@ -7409,12 +7632,12 @@ function sizeBlock2(sizes, size) {
7409
7632
  import { execFileSync as execFileSync4 } from "child_process";
7410
7633
 
7411
7634
  // src/commands/ship.ts
7412
- import { existsSync as existsSync24, readdirSync as readdirSync15, readFileSync as readFileSync33 } from "fs";
7413
- import { join as join40 } from "path";
7635
+ import { existsSync as existsSync25, readdirSync as readdirSync15, readFileSync as readFileSync34 } from "fs";
7636
+ import { join as join41 } from "path";
7414
7637
 
7415
7638
  // src/commands/seo.ts
7416
- import { readFileSync as readFileSync32 } from "fs";
7417
- import { join as join39 } from "path";
7639
+ import { readFileSync as readFileSync33 } from "fs";
7640
+ import { join as join40 } from "path";
7418
7641
  var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
7419
7642
  var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
7420
7643
  var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
@@ -7455,7 +7678,7 @@ function seo(opts) {
7455
7678
  for (const file of files) {
7456
7679
  let raw;
7457
7680
  try {
7458
- raw = readFileSync32(join39(root, file), "utf8");
7681
+ raw = readFileSync33(join40(root, file), "utf8");
7459
7682
  } catch {
7460
7683
  continue;
7461
7684
  }
@@ -7520,7 +7743,7 @@ function critiqueAtShip(projectRoot, specBody) {
7520
7743
  const own = specCritique(specBody);
7521
7744
  if (own) return own === "at-ship";
7522
7745
  try {
7523
- return JSON.parse(readFileSync33(join40(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
7746
+ return JSON.parse(readFileSync34(join41(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
7524
7747
  } catch {
7525
7748
  return false;
7526
7749
  }
@@ -7528,14 +7751,14 @@ function critiqueAtShip(projectRoot, specBody) {
7528
7751
  function tweakDeferred(dir) {
7529
7752
  let t;
7530
7753
  try {
7531
- t = JSON.parse(readFileSync33(join40(dir, "tweak.json"), "utf8"));
7754
+ t = JSON.parse(readFileSync34(join41(dir, "tweak.json"), "utf8"));
7532
7755
  } catch {
7533
7756
  return false;
7534
7757
  }
7535
7758
  if (t.deferred === void 0 || t.deferred === false) return false;
7536
7759
  let judged;
7537
7760
  try {
7538
- judged = JSON.parse(readFileSync33(join40(dir, "verdicts.lock"), "utf8")).tweak;
7761
+ judged = JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8")).tweak;
7539
7762
  } catch {
7540
7763
  }
7541
7764
  return judged !== t.at;
@@ -7546,15 +7769,16 @@ function pageChecksum(html) {
7546
7769
  function changedSinceLock(projectRoot, dir) {
7547
7770
  let lock;
7548
7771
  try {
7549
- lock = JSON.parse(readFileSync33(join40(dir, "verdicts.lock"), "utf8"));
7772
+ lock = JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8"));
7550
7773
  } catch {
7551
7774
  return void 0;
7552
7775
  }
7553
7776
  const file = lock.page?.file;
7554
7777
  if (!file || !lock.page?.checksum) return void 0;
7555
7778
  try {
7556
- const html = readFileSync33(join40(projectRoot, file), "utf8");
7557
- return pageChecksum(html) !== lock.page.checksum && checksum(html) !== lock.page.checksum ? file : void 0;
7779
+ const html = readFileSync34(join41(projectRoot, file), "utf8");
7780
+ const seen = [pageChecksum(html), checksum(html)];
7781
+ return seen.includes(lock.page.checksum) ? void 0 : file;
7558
7782
  } catch {
7559
7783
  return void 0;
7560
7784
  }
@@ -7564,7 +7788,7 @@ function supersededBy(front) {
7564
7788
  return named ? named : void 0;
7565
7789
  }
7566
7790
  function pageStatus(projectRoot, surface) {
7567
- const spec = readFileSync33(join40(projectRoot, ".jig", "specs", `${surface}.spec.md`), "utf8");
7791
+ const spec = readText(join41(projectRoot, ".jig", "specs", `${surface}.spec.md`));
7568
7792
  const front = spec.split(/^---\s*$/m)[1] ?? "";
7569
7793
  if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
7570
7794
  return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
@@ -7572,10 +7796,10 @@ function pageStatus(projectRoot, surface) {
7572
7796
  const successor = supersededBy(front);
7573
7797
  if (successor !== void 0) {
7574
7798
  const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
7575
- return existsSync24(join40(projectRoot, ".jig", "specs", file)) ? { surface, state: "superseded", detail: `superseded by ${file}, whose critique judges its page` } : { surface, state: "incomplete", detail: `\`superseded_by: ${successor}\` names a spec that does not exist in .jig/specs` };
7799
+ return existsSync25(join41(projectRoot, ".jig", "specs", file)) ? { surface, state: "superseded", detail: `superseded by ${file}, whose critique judges its page` } : { surface, state: "incomplete", detail: `\`superseded_by: ${successor}\` names a spec that does not exist in .jig/specs` };
7576
7800
  }
7577
- const dir = join40(projectRoot, ".jig", "critique", surface);
7578
- if (!existsSync24(join40(dir, "screen.json")) && !existsSync24(join40(dir, "code.json"))) {
7801
+ const dir = join41(projectRoot, ".jig", "critique", surface);
7802
+ if (!existsSync25(join41(dir, "screen.json")) && !existsSync25(join41(dir, "code.json"))) {
7579
7803
  return { surface, state: "never", detail: "never critiqued" };
7580
7804
  }
7581
7805
  if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
@@ -7585,7 +7809,7 @@ function pageStatus(projectRoot, surface) {
7585
7809
  const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
7586
7810
  let judgedPage = false;
7587
7811
  try {
7588
- judgedPage = !!JSON.parse(readFileSync33(join40(dir, "verdicts.lock"), "utf8")).page?.checksum;
7812
+ judgedPage = !!JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8")).page?.checksum;
7589
7813
  } catch {
7590
7814
  }
7591
7815
  if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
@@ -7601,17 +7825,19 @@ function ship(opts) {
7601
7825
  const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
7602
7826
  const seoResult = seo({ projectRoot: root });
7603
7827
  const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
7604
- const specsDir = join40(root, ".jig", "specs");
7605
- const surfaces = existsSync24(specsDir) ? readdirSync15(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
7828
+ const specsDir = join41(root, ".jig", "specs");
7829
+ const surfaces = existsSync25(specsDir) ? readdirSync15(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
7606
7830
  const pages = surfaces.map((s) => pageStatus(root, s));
7607
7831
  const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
7608
- const ready = mechanicalErrors === 0 && seoErrors === 0 && owed.length === 0;
7832
+ const agentFiles = agentFileProblems(root);
7833
+ const ready = mechanicalErrors === 0 && seoErrors === 0 && owed.length === 0 && agentFiles.length === 0;
7609
7834
  const count = (state) => pages.filter((p) => p.state === state).length;
7610
7835
  const line = `JIG_SHIP: ready=${ready ? "yes" : "no"} mechanical=${mechanicalErrors} seo=${seoErrors} pages=${pages.length} judged=${count("judged")} owed=${owed.length} in-progress=${count("in-progress")} superseded=${count("superseded")}`;
7611
7836
  const mark = { judged: "\u2713", never: "\u2717", changed: "\u2717", reprobe: "\u2717", deferred: "\u2717", incomplete: "\u2717", findings: "\u2717", "in-progress": "\xB7", superseded: "\xB7" };
7612
7837
  const report2 = [
7613
7838
  ` ${mechanicalErrors === 0 ? "\u2713" : "\u2717"} check --all --ci: ${mechanicalErrors} mechanical error${mechanicalErrors === 1 ? "" : "s"}`,
7614
7839
  ` ${seoErrors === 0 ? "\u2713" : "\u2717"} seo: ${seoErrors} error${seoErrors === 1 ? "" : "s"}`,
7840
+ ...agentFiles.map((p) => ` \u2717 ${p}`),
7615
7841
  ...pages.map((p) => ` ${mark[p.state]} ${p.surface}: ${p.detail}`),
7616
7842
  owed.length ? ` Critique each page marked \u2717 (\`/jig critique <page>\`); fix what it finds, or record the owner's ruling, and run \`jig ship\` again.` : "",
7617
7843
  ` Not checked by Jig, here or anywhere: ${NOT_COVERED}.`,
@@ -7621,11 +7847,37 @@ function ship(opts) {
7621
7847
  }
7622
7848
 
7623
7849
  // src/check/spec-checked.ts
7624
- import { existsSync as existsSync25, readFileSync as readFileSync34 } from "fs";
7850
+ import { existsSync as existsSync26, readFileSync as readFileSync35 } from "fs";
7625
7851
  import { createHash as createHash2 } from "crypto";
7626
- import { join as join41 } from "path";
7852
+ import { join as join42 } from "path";
7853
+
7854
+ // src/check/owner-word.ts
7855
+ var SAYS = {
7856
+ confirm: /\b(confirm(s|ed)?|yes|yep|approve[ds]?|looks (good|right|fine)|lgtm|go ahead|agreed|that['’]?s right|correct|ok(ay)?)\b|^\s*go\b/im,
7857
+ approve: /\b(approve[ds]?|looks (good|right|fine)|lgtm|yes|go ahead|good to go|confirm(s|ed)?|agreed|build it|that['’]?s (it|right)|ok(ay)?)\b/i,
7858
+ skip: /\bskip|\bno (mockup|drawing)|without (a )?(mockup|drawing)|(don['’]?t|do not|no need to) (draw|mock)|(mockup|drawing) (isn['’]?t|is not) needed/i,
7859
+ defer: /\b(later|at ship|defer(red)?|wait|leave (it|the critique|the re-?judge)|(don['’]?t|do not) (re-?judge|critique)|no (re-?judge|critique)|skip the (re-?judge|critique))\b/i
7860
+ };
7861
+ function saysSo(word, text) {
7862
+ return SAYS[word].test(text);
7863
+ }
7864
+ var WHAT = {
7865
+ confirm: "confirm the spec",
7866
+ approve: "approve the drawing",
7867
+ skip: "skip the mockup",
7868
+ defer: "leave the re-judge for later"
7869
+ };
7870
+ function ownerWordProblem(word, quoted, owner, where) {
7871
+ const q = quoted.replace(/^["“]|["”]$/g, "").trim();
7872
+ if (!q) return `${where} records the owner's word to ${WHAT[word]} without their words. Quote what they said.`;
7873
+ if (!quoteHeld(q, owner)) return `${where} quotes "${q}", which the owner did not say in this session. Quote their reply as they wrote it; a word nobody gave is not one.`;
7874
+ if (!saysSo(word, q)) return `${where} quotes "${q}", which does not ${WHAT[word]}. Words the owner said about something else are not their word on this: ask them, and quote the answer.`;
7875
+ return void 0;
7876
+ }
7877
+
7878
+ // src/check/spec-checked.ts
7627
7879
  function specChecksum(body) {
7628
- return createHash2("sha256").update(body.replace(/^\s*(confirmed|mockup)\s*:.*$/gim, "")).digest("hex");
7880
+ return createHash2("sha256").update(body.replace(/\r\n/g, "\n").replace(/^\s*(confirmed|mockup)\s*:.*$/gim, "")).digest("hex");
7629
7881
  }
7630
7882
  var mockupLine = (body) => /^\s*mockup\s*:\s*(.*)$/im.exec(body.split(/^---\s*$/m)[1] ?? "")?.[1]?.trim() ?? "";
7631
7883
  function mockupWordProblems(spec, then, owner) {
@@ -7638,10 +7890,8 @@ function mockupWordProblems(spec, then, owner) {
7638
7890
  if (!quoted) {
7639
7891
  return [`${spec.path}: \`mockup: ${word}\` is recorded without the owner's words. Write \`mockup: ${word} \u2014 "<what they said>"\`, quoting the reply that ${word === "approved" ? "approved it" : "said to skip it"}.`];
7640
7892
  }
7641
- if (!quoteHeld(quoted, owner)) {
7642
- return [`${spec.path}: \`mockup: ${word}\` quotes "${quoted}", which the owner did not say in this session. Quote their reply as they wrote it; ${word === "approved" ? "an approval" : "a skip"} nobody gave is not one.`];
7643
- }
7644
- return [];
7893
+ const problem = ownerWordProblem(word === "approved" ? "approve" : "skip", quoted, owner, `${spec.path}: \`mockup: ${word}\``);
7894
+ return problem ? [problem] : [];
7645
7895
  }
7646
7896
  function mockupPending(body) {
7647
7897
  const now = mockupLine(body);
@@ -7676,7 +7926,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
7676
7926
  const recordPath = `.jig/specs/${spec.slug}.checked.json`;
7677
7927
  let record;
7678
7928
  try {
7679
- record = JSON.parse(readFileSync34(join41(root, recordPath), "utf8"));
7929
+ record = JSON.parse(readFileSync35(join42(root, recordPath), "utf8"));
7680
7930
  } catch {
7681
7931
  problems.push(
7682
7932
  `${recordPath} is missing. Before the owner is asked to confirm, a reader who did not write the spec checks its quotations against the owner's words and its facts against their sources, and writes that record (step 3c). The owner checks the spec by the sheet it gives them.`
@@ -7705,16 +7955,16 @@ function specCheckProblems(root, spec, then, owner, decisions) {
7705
7955
  if (unsaid.length) problems.push(`${recordPath}: ${unsaid.length} unchecked ${unsaid.length === 1 ? "fact says" : "facts say"} nothing of why. Say in \`note\` why the source could not be opened; the owner opens it instead.`);
7706
7956
  const missing = facts.map((f) => typeof f?.source === "string" ? f.source.trim() : "").filter((s) => s && !/^[a-z][a-z0-9+.-]*:\/\//i.test(s)).filter((s) => {
7707
7957
  const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
7708
- if (!file || !existsSync25(join41(root, file))) return true;
7958
+ if (!file || !existsSync26(join42(root, file))) return true;
7709
7959
  if (!line) return false;
7710
7960
  try {
7711
- return Number(line) > readFileSync34(join41(root, file), "utf8").split("\n").length;
7961
+ return Number(line) > readFileSync35(join42(root, file), "utf8").split("\n").length;
7712
7962
  } catch {
7713
7963
  return true;
7714
7964
  }
7715
7965
  });
7716
7966
  if (missing.length) {
7717
- problems.push(`${recordPath} cites ${missing.length === 1 ? "a source" : "sources"} the project does not have: ${missing.slice(0, 4).join(", ")}. A fact is checked at a source that exists; name it as a path from the project root, or a URL.`);
7967
+ problems.push(`${recordPath} cites ${missing.length === 1 ? "a source" : "sources"} the project does not have: ${missing.slice(0, 4).join(", ")}. A fact is checked at a source that exists: \`source\` is a path from the project root, \`path:line\`, or a URL, and nothing else; what you found there goes in \`note\`.`);
7718
7968
  }
7719
7969
  return problems;
7720
7970
  }
@@ -7722,10 +7972,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
7722
7972
  // src/commands/gate.ts
7723
7973
  var MAX_BLOCKS = 3;
7724
7974
  function lastJigInvocation(transcriptPath) {
7725
- if (!transcriptPath || !existsSync26(transcriptPath)) return void 0;
7975
+ if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
7726
7976
  let text;
7727
7977
  try {
7728
- text = readFileSync35(transcriptPath, "utf8");
7978
+ text = readFileSync36(transcriptPath, "utf8");
7729
7979
  } catch {
7730
7980
  return void 0;
7731
7981
  }
@@ -7750,10 +8000,10 @@ function lastJigInvocation(transcriptPath) {
7750
8000
  return found;
7751
8001
  }
7752
8002
  function ownerWords(transcriptPath) {
7753
- if (!transcriptPath || !existsSync26(transcriptPath)) return "";
8003
+ if (!transcriptPath || !existsSync27(transcriptPath)) return "";
7754
8004
  const said = [];
7755
8005
  try {
7756
- for (const line of readFileSync35(transcriptPath, "utf8").split("\n")) {
8006
+ for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
7757
8007
  if (!line.includes('"user"')) continue;
7758
8008
  let entry;
7759
8009
  try {
@@ -7773,11 +8023,46 @@ function ownerWords(transcriptPath) {
7773
8023
  }
7774
8024
  return said.join("\n");
7775
8025
  }
8026
+ function namedSpec(root, surface) {
8027
+ if (!surface) return specFor(root, void 0);
8028
+ return existsSync27(join43(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
8029
+ }
8030
+ function isConfirmed(body) {
8031
+ return /^\s*confirmed\s*:\s*true\b/im.test(body.split(/^---\s*$/m)[1] ?? "");
8032
+ }
8033
+ function ownerReplyAfter(transcriptPath, asked) {
8034
+ if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
8035
+ const turns = [];
8036
+ try {
8037
+ for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
8038
+ let entry;
8039
+ try {
8040
+ entry = JSON.parse(line);
8041
+ } catch {
8042
+ continue;
8043
+ }
8044
+ if (entry.type !== "user" && entry.type !== "assistant" || entry.isMeta) continue;
8045
+ const content = entry.message?.content;
8046
+ const texts = typeof content === "string" ? [content] : Array.isArray(content) ? content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text) : [];
8047
+ for (const text of texts) turns.push({ who: entry.type === "user" ? "owner" : "agent", text });
8048
+ }
8049
+ } catch {
8050
+ return void 0;
8051
+ }
8052
+ let answer;
8053
+ turns.forEach((t, i) => {
8054
+ if (t.who !== "agent" || !asked.test(t.text)) return;
8055
+ const reply = [];
8056
+ for (let j = i + 1; j < turns.length && turns[j].who === "owner"; j++) reply.push(turns[j].text);
8057
+ if (reply.length) answer = reply.join("\n");
8058
+ });
8059
+ return answer;
8060
+ }
7776
8061
  function lastAssistantText(transcriptPath) {
7777
- if (!transcriptPath || !existsSync26(transcriptPath)) return void 0;
8062
+ if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
7778
8063
  let found;
7779
8064
  try {
7780
- for (const line of readFileSync35(transcriptPath, "utf8").split("\n")) {
8065
+ for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
7781
8066
  if (!line.includes('"assistant"')) continue;
7782
8067
  let entry;
7783
8068
  try {
@@ -7801,9 +8086,9 @@ function asksOwner(text) {
7801
8086
  }
7802
8087
  var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak"]);
7803
8088
  function sessionStart(transcriptPath) {
7804
- if (!transcriptPath || !existsSync26(transcriptPath)) return void 0;
8089
+ if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
7805
8090
  try {
7806
- for (const line of readFileSync35(transcriptPath, "utf8").split("\n")) {
8091
+ for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
7807
8092
  const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
7808
8093
  if (m) {
7809
8094
  const t = Date.parse(m[1]);
@@ -7818,20 +8103,20 @@ function verdictsMtime(dir) {
7818
8103
  let newest = 0;
7819
8104
  for (const name of VERDICT_FILES) {
7820
8105
  try {
7821
- newest = Math.max(newest, statSync5(join42(dir, name)).mtimeMs);
8106
+ newest = Math.max(newest, statSync5(join43(dir, name)).mtimeMs);
7822
8107
  } catch {
7823
8108
  }
7824
8109
  }
7825
8110
  return newest;
7826
8111
  }
7827
8112
  function surfacesInPlay(root, command, transcriptPath, surface) {
7828
- const critiqueDir = join42(root, ".jig", "critique");
7829
- if (!existsSync26(critiqueDir)) return [];
8113
+ const critiqueDir = join43(root, ".jig", "critique");
8114
+ if (!existsSync27(critiqueDir)) return [];
7830
8115
  const all = readdirSync16(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
7831
8116
  const start = sessionStart(transcriptPath);
7832
8117
  if (start === void 0) return all;
7833
8118
  const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
7834
- return all.filter((s) => s === current || verdictsMtime(join42(critiqueDir, s)) >= start - 1e3);
8119
+ return all.filter((s) => s === current || verdictsMtime(join43(critiqueDir, s)) >= start - 1e3);
7835
8120
  }
7836
8121
  function surfacesToProbe(root, input) {
7837
8122
  const invocation = lastJigInvocation(input.transcript_path);
@@ -7839,9 +8124,13 @@ function surfacesToProbe(root, input) {
7839
8124
  }
7840
8125
  function fileAtSessionStart(root, path, start) {
7841
8126
  const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8127
+ try {
8128
+ if (start !== void 0 && statSync5(join43(root, path)).mtimeMs < start - 1e3) return readText(join43(root, path));
8129
+ } catch {
8130
+ }
7842
8131
  try {
7843
8132
  const base = start === void 0 ? "HEAD" : git(["rev-list", "-1", `--before=@${Math.floor(start / 1e3)}`, "HEAD"]).trim();
7844
- return base ? git(["show", `${base}:./${path}`]) : "";
8133
+ return base ? lf(git(["show", `${base}:./${path}`])) : "";
7845
8134
  } catch {
7846
8135
  return "";
7847
8136
  }
@@ -7854,7 +8143,7 @@ function changedOutsideRecords(root, start) {
7854
8143
  const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
7855
8144
  const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
7856
8145
  try {
7857
- return start === void 0 || statSync5(join42(root, f)).mtimeMs >= start - 1e3;
8146
+ return start === void 0 || statSync5(join43(root, f)).mtimeMs >= start - 1e3;
7858
8147
  } catch {
7859
8148
  return false;
7860
8149
  }
@@ -7869,8 +8158,8 @@ function drawingChangedAfterApproval(root, specPath, at) {
7869
8158
  try {
7870
8159
  const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
7871
8160
  if (!approvedIn) return void 0;
7872
- const then = git(["show", `${approvedIn}:./${at}`]);
7873
- return then !== readFileSync35(join42(root, at), "utf8") ? approvedIn.slice(0, 7) : void 0;
8161
+ const then = lf(git(["show", `${approvedIn}:./${at}`]));
8162
+ return then !== readText(join43(root, at)) ? approvedIn.slice(0, 7) : void 0;
7874
8163
  } catch {
7875
8164
  return void 0;
7876
8165
  }
@@ -7882,18 +8171,25 @@ function commandProblems(root, command, surface, start, owner = "") {
7882
8171
  const found = decisionsFile(root);
7883
8172
  if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
7884
8173
  else {
7885
- const body = readFileSync35(join42(root, found), "utf8");
8174
+ const body = readText(join43(root, found));
7886
8175
  if (!/^##\s+Unresolved\s*$/im.test(body)) {
7887
8176
  problems.push("DECISIONS.md has no `## Unresolved` section. Round 3 asks by name what is still undecided; write what the owner named, or `None named by the owner.`");
7888
8177
  }
7889
8178
  if (/\[TODO\]/.test(body)) problems.push("DECISIONS.md still contains [TODO] markers.");
7890
- problems.push(...unsourcedReasons(body, fileAtSessionStart(root, found, start), { newWhysOnly: true }).map((p) => `DECISIONS.md: ${p}`));
8179
+ const then = fileAtSessionStart(root, found, start);
8180
+ problems.push(...unsourcedReasons(body, then, { newWhysOnly: true }).map((p) => `DECISIONS.md: ${p}`));
8181
+ if (owner) problems.push(...quotesNotFrom(body, then, owner, "the owner's words in this session").map((p) => `DECISIONS.md: ${p}`));
7891
8182
  }
7892
8183
  }
8184
+ if (command === "spec" && surface && !existsSync27(join43(root, ".jig", "specs", `${surface}.spec.md`))) {
8185
+ problems.push(`spec wrote no .jig/specs/${surface}.spec.md. The spec is that file: a draft anywhere else is read by no later step and checked by nothing.`);
8186
+ return problems;
8187
+ }
7893
8188
  if (command === "spec" || command === "mockup" || command === "make" || command === "critique" || command === "tweak") {
7894
8189
  if (!spec) problems.push(`${command} needs a spec: there is no file in .jig/specs/.`);
7895
8190
  else {
7896
8191
  problems.push(...specProblems(spec));
8192
+ problems.push(...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start)));
7897
8193
  if (problems.length === 0) problems.push(...navProblems(spec));
7898
8194
  }
7899
8195
  }
@@ -7908,12 +8204,12 @@ function commandProblems(root, command, surface, start, owner = "") {
7908
8204
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
7909
8205
  if (/^approved/i.test(mockup)) {
7910
8206
  if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
7911
- else if (!/^https?:/i.test(at) && !existsSync26(join42(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
8207
+ else if (!/^https?:/i.test(at) && !existsSync27(join43(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
7912
8208
  else if (!/^https?:/i.test(at) && !at.startsWith(".jig/mockups/")) problems.push(`The drawing is at ${at}. A mockup lives in .jig/mockups/, outside what check scans and outside what ships.`);
7913
8209
  }
7914
- if (at && /\.html?$/i.test(at) && existsSync26(join42(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
8210
+ if (at && /\.html?$/i.test(at) && existsSync27(join43(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
7915
8211
  }
7916
- if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner));
8212
+ if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
7917
8213
  if ((command === "spec" || command === "make") && spec) {
7918
8214
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
7919
8215
  const mockup = /^\s*mockup\s*:\s*(\S+)/im.exec(front)?.[1] ?? "";
@@ -7939,7 +8235,7 @@ function commandProblems(root, command, surface, start, owner = "") {
7939
8235
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
7940
8236
  const mockup = /^\s*mockup\s*:\s*(\S+)/im.exec(front)?.[1] ?? "";
7941
8237
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
7942
- if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync26(join42(root, at))) {
8238
+ if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync27(join43(root, at))) {
7943
8239
  const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
7944
8240
  if (changedAfter) {
7945
8241
  problems.push(`${at} has changed since the owner approved it (${changedAfter}). An approval is of the drawing as the owner saw it: set \`mockup: pending\` in ${spec.path} and ask again.`);
@@ -7951,7 +8247,7 @@ function commandProblems(root, command, surface, start, owner = "") {
7951
8247
  if (found) {
7952
8248
  let now = "";
7953
8249
  try {
7954
- now = readFileSync35(join42(root, found), "utf8");
8250
+ now = readText(join43(root, found));
7955
8251
  } catch {
7956
8252
  }
7957
8253
  const then = fileAtSessionStart(root, found, start);
@@ -7965,13 +8261,13 @@ function commandProblems(root, command, surface, start, owner = "") {
7965
8261
  if (found) {
7966
8262
  let now = "";
7967
8263
  try {
7968
- now = readFileSync35(join42(root, found), "utf8");
8264
+ now = readText(join43(root, found));
7969
8265
  } catch {
7970
8266
  }
7971
8267
  const then = fileAtSessionStart(root, found, start);
7972
8268
  if (now && now !== then) {
7973
8269
  problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
7974
- const change = readTweak(join42(root, ".jig", "critique", spec.slug))?.change;
8270
+ const change = readTweak(join43(root, ".jig", "critique", spec.slug))?.change;
7975
8271
  if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
7976
8272
  }
7977
8273
  }
@@ -7980,7 +8276,7 @@ function commandProblems(root, command, surface, start, owner = "") {
7980
8276
  for (const file of modeFiles(root)) {
7981
8277
  let now = "";
7982
8278
  try {
7983
- now = readFileSync35(join42(root, file), "utf8");
8279
+ now = readText(join43(root, file));
7984
8280
  } catch {
7985
8281
  continue;
7986
8282
  }
@@ -7991,8 +8287,8 @@ function commandProblems(root, command, surface, start, owner = "") {
7991
8287
  }
7992
8288
  }
7993
8289
  if (command === "critique") {
7994
- const dir = join42(root, ".jig", "critique");
7995
- const surfaces = existsSync26(dir) ? readdirSync16(dir).filter((s) => existsSync26(join42(dir, s, "screen.json")) || existsSync26(join42(dir, s, "code.json"))) : [];
8290
+ const dir = join43(root, ".jig", "critique");
8291
+ const surfaces = existsSync27(dir) ? readdirSync16(dir).filter((s) => existsSync27(join43(dir, s, "screen.json")) || existsSync27(join43(dir, s, "code.json"))) : [];
7996
8292
  for (const surface2 of surfaces) {
7997
8293
  const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
7998
8294
  if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
@@ -8003,7 +8299,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8003
8299
  }
8004
8300
  }
8005
8301
  const own = specFor(root, surface)?.slug;
8006
- if (own && surfaces.includes(own)) problems.push(...reportOmissions(join42(dir, own), own));
8302
+ if (own && surfaces.includes(own)) problems.push(...reportOmissions(join43(dir, own), own));
8007
8303
  if (surfaces.length === 0) {
8008
8304
  problems.push("critique wrote no verdict files. Each reader arm writes .jig/critique/<surface>/screen.json or code.json, and `jig verdicts <surface>` \u2014 not your own count \u2014 decides whether the review is complete. A report without them is not a review.");
8009
8305
  }
@@ -8016,7 +8312,7 @@ function drawingBeforeAsking(root, surface) {
8016
8312
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
8017
8313
  const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8018
8314
  const at = named || `.jig/mockups/${spec.slug}.html`;
8019
- if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync26(join42(root, at))) return [];
8315
+ if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync27(join43(root, at))) return [];
8020
8316
  const found = mockupDrawingProblems(root, spec.body, at);
8021
8317
  return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
8022
8318
  }
@@ -8024,14 +8320,14 @@ function specChecked(root, spec, start, owner) {
8024
8320
  const found = decisionsFile(root);
8025
8321
  let decisions = "";
8026
8322
  try {
8027
- if (found) decisions = readFileSync35(join42(root, found), "utf8");
8323
+ if (found) decisions = readText(join43(root, found));
8028
8324
  } catch {
8029
8325
  }
8030
8326
  return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
8031
8327
  }
8032
8328
  function modeFiles(root) {
8033
8329
  try {
8034
- const state = JSON.parse(readFileSync35(join42(root, ".jig", "state.json"), "utf8"));
8330
+ const state = JSON.parse(readFileSync36(join43(root, ".jig", "state.json"), "utf8"));
8035
8331
  return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
8036
8332
  } catch {
8037
8333
  return [];
@@ -8040,7 +8336,7 @@ function modeFiles(root) {
8040
8336
  function reportOmissions(dir, surface) {
8041
8337
  let report2;
8042
8338
  try {
8043
- report2 = readFileSync35(join42(dir, "REPORT.md"), "utf8").toLowerCase();
8339
+ report2 = readText(join43(dir, "REPORT.md")).toLowerCase();
8044
8340
  } catch {
8045
8341
  return [];
8046
8342
  }
@@ -8048,7 +8344,7 @@ function reportOmissions(dir, surface) {
8048
8344
  for (const f of VERDICT_FILES) {
8049
8345
  let file;
8050
8346
  try {
8051
- file = JSON.parse(readFileSync35(join42(dir, f), "utf8"));
8347
+ file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
8052
8348
  } catch {
8053
8349
  continue;
8054
8350
  }
@@ -8061,7 +8357,7 @@ function reportOmissions(dir, surface) {
8061
8357
  if (!missing.length) return [];
8062
8358
  return [`.jig/critique/${surface}/REPORT.md leaves out ${missing.length === 1 ? "a finding" : `${missing.length} findings`} the verdict files hold: ${missing.slice(0, 8).join(", ")}. The report is what the owner and \`make\` read; name every finding in it, by id (a decision by its name).`];
8063
8359
  }
8064
- function tweakProblems(root, spec, owner = "") {
8360
+ function tweakProblems(root, spec, owner = "", start) {
8065
8361
  const problems = [];
8066
8362
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
8067
8363
  if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) problems.push(`${spec.path} is not confirmed. A tweak changes a page the owner has confirmed; an unconfirmed spec goes through \`spec\`.`);
@@ -8069,21 +8365,31 @@ function tweakProblems(root, spec, owner = "") {
8069
8365
  if (!/^(approved|skipped)/i.test(mockup)) problems.push(`${spec.path}: \`mockup:\` is ${mockup || "empty"}. A tweak changes a page whose drawing the owner has approved (or skipped); take a new page through \`mockup\` and \`make\`.`);
8070
8366
  else problems.push(...structureSinceApproval(root, spec, front));
8071
8367
  const surface = spec.slug;
8072
- const record = readTweak(join42(root, ".jig", "critique", surface));
8368
+ const record = readTweak(join43(root, ".jig", "critique", surface));
8073
8369
  if (!record) {
8074
8370
  problems.push(`.jig/critique/${surface}/tweak.json is missing. It names the change in the owner's words (\`change\`), when it was made (\`at\`) and the rule ids and decisions re-judged for it (\`ids\`).`);
8075
8371
  } else {
8076
8372
  if (!record.change) problems.push(`.jig/critique/${surface}/tweak.json has no \`change\`: the owner's words for what changed.`);
8373
+ else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(join43(root, ".jig", "critique", surface, "tweak.json")) && !quoteHeld(record.change.replace(/^["“]|["”]$/g, ""), owner)) {
8374
+ problems.push(`.jig/critique/${surface}/tweak.json: \`change\` is "${record.change.length > 90 ? `${record.change.slice(0, 90)}\u2026` : record.change}", which the owner did not say in this session. It is their words for the change, as they gave them; what you made of them goes in the spec's Tweak entry.`);
8375
+ }
8376
+ try {
8377
+ const raw = JSON.parse(readFileSync36(join43(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
8378
+ const extra = Object.keys(raw).filter((k) => !["at", "change", "ids", "deferred"].includes(k));
8379
+ if (extra.length) problems.push(`.jig/critique/${surface}/tweak.json: ${extra.map((k) => `\`${k}\``).join(", ")} ${extra.length === 1 ? "is not a field" : "are not fields"} Jig reads. tweak.json holds \`at\`, \`change\`, \`ids\` and \`deferred\`; what else you have to say goes in the spec's Tweak entry.`);
8380
+ } catch {
8381
+ }
8077
8382
  if (!record.ids.length) problems.push(`.jig/critique/${surface}/tweak.json names nothing to re-judge. A change that no rule and no decision could see needs no tweak; name the ones it can.`);
8078
8383
  if (record.deferred !== void 0) {
8079
- if (typeof record.deferred === "string" ? !record.deferred.trim() || !quoteHeld(record.deferred.replace(/^["“]|["”]$/g, ""), owner) : !critiqueAtShip(root, spec.body)) {
8384
+ const said = typeof record.deferred === "string" ? ownerWordProblem("defer", record.deferred, owner, `.jig/critique/${surface}/tweak.json: \`deferred\``) : void 0;
8385
+ if (typeof record.deferred === "string" ? said !== void 0 : !critiqueAtShip(root, spec.body)) {
8080
8386
  problems.push(
8081
- `.jig/critique/${surface}/tweak.json defers its re-judge, and nobody said to. \`deferred\` quotes the owner telling you to leave it for later, or is \`true\` where the page's spec says \`critique: at-ship\` (or, the spec silent, jig.config.json says \`"critique": "at-ship"\`). Otherwise re-judge what it names.`
8387
+ `.jig/critique/${surface}/tweak.json defers its re-judge, and nobody said to. \`deferred\` quotes the owner telling you to leave it for later, or is \`true\` where the page's spec says \`critique: at-ship\` (or, the spec silent, jig.config.json says \`"critique": "at-ship"\`). Otherwise re-judge what it names.` + (said ? ` ${said}` : "")
8082
8388
  );
8083
8389
  }
8084
8390
  return problems;
8085
8391
  }
8086
- const unjudged = unjudgedTweakIds(join42(root, ".jig", "critique", surface), record);
8392
+ const unjudged = unjudgedTweakIds(join43(root, ".jig", "critique", surface), record);
8087
8393
  if (unjudged.length) {
8088
8394
  problems.push(
8089
8395
  `.jig/critique/${surface}: tweak.json names ${unjudged.slice(0, 6).join(", ")}${unjudged.length > 6 ? " and more" : ""} but no verdict for ${unjudged.length === 1 ? "it" : "them"} carries \`"tweak": "${record.at}"\`. Each named verdict is re-judged on the changed page by a reader that did not make the change.`
@@ -8110,7 +8416,7 @@ function structureSinceApproval(root, spec, front) {
8110
8416
  const problems = [];
8111
8417
  let thenBody = "";
8112
8418
  try {
8113
- thenBody = git(["show", `${baseline}:./${spec.path}`]);
8419
+ thenBody = lf(git(["show", `${baseline}:./${spec.path}`]));
8114
8420
  } catch {
8115
8421
  return [];
8116
8422
  }
@@ -8119,9 +8425,9 @@ function structureSinceApproval(root, spec, front) {
8119
8425
  problems.push(`${spec.path}: the regions under \`sizes:\` differ from the ones the page had ${when}. A change to what the page holds is not a tweak: take it through \`spec\` and \`mockup\`.`);
8120
8426
  }
8121
8427
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8122
- if (at && !/^https?:/i.test(at) && existsSync26(join42(root, at))) {
8428
+ if (at && !/^https?:/i.test(at) && existsSync27(join43(root, at))) {
8123
8429
  try {
8124
- if (git(["show", `${baseline}:./${at}`]) !== readFileSync35(join42(root, at), "utf8")) {
8430
+ if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join43(root, at))) {
8125
8431
  problems.push(`${at} has changed since ${when.replace(/^when /, "")}. A tweak leaves the drawing as approved; a change the drawing must show goes through \`mockup\`.`);
8126
8432
  }
8127
8433
  } catch {
@@ -8131,7 +8437,7 @@ function structureSinceApproval(root, spec, front) {
8131
8437
  }
8132
8438
  function readTweak(dir) {
8133
8439
  try {
8134
- const raw = JSON.parse(readFileSync35(join42(dir, "tweak.json"), "utf8"));
8440
+ const raw = JSON.parse(readFileSync36(join43(dir, "tweak.json"), "utf8"));
8135
8441
  const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
8136
8442
  return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
8137
8443
  } catch {
@@ -8144,20 +8450,20 @@ function surfacePage(projectRoot, surface) {
8144
8450
  const declared = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8145
8451
  const candidates = [declared, `${surface}.html`, declared ? `${declared.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared ? /\.\w+$/.test(c) : true));
8146
8452
  for (const candidate of candidates) {
8147
- if (candidate && existsSync26(join42(projectRoot, candidate))) return candidate;
8453
+ if (candidate && existsSync27(join43(projectRoot, candidate))) return candidate;
8148
8454
  }
8149
8455
  return void 0;
8150
8456
  }
8151
8457
  function gate(opts) {
8152
8458
  const root = opts.projectRoot;
8153
- if (!existsSync26(join42(root, "jig.config.json")) && !existsSync26(join42(root, ".jig"))) {
8459
+ if (!existsSync27(join43(root, "jig.config.json")) && !existsSync27(join43(root, ".jig"))) {
8154
8460
  return { block: false, reason: "" };
8155
8461
  }
8156
8462
  const invocation = lastJigInvocation(opts.input.transcript_path);
8157
8463
  const command = invocation?.command;
8158
8464
  const waiting = command !== void 0 && ASKS_THE_OWNER.has(command) && asksOwner(lastAssistantText(opts.input.transcript_path));
8159
8465
  const start = sessionStart(opts.input.transcript_path);
8160
- const owner = command === "spec" || command === "mockup" || command === "make" || command === "tweak" ? ownerWords(opts.input.transcript_path) : "";
8466
+ const owner = command === "decide" || command === "spec" || command === "mockup" || command === "make" || command === "tweak" ? ownerWords(opts.input.transcript_path) : "";
8161
8467
  const problems = command && !waiting ? commandProblems(root, command, invocation?.surface, start, owner).map((p) => `/jig ${command}: ${p}`) : [];
8162
8468
  if (waiting && command === "mockup") problems.push(...drawingBeforeAsking(root, invocation?.surface).map((p) => `/jig mockup: ${p}`));
8163
8469
  if (command === "ship" && !asksOwner(lastAssistantText(opts.input.transcript_path))) {
@@ -8171,9 +8477,24 @@ ${result.report}`);
8171
8477
  problems.push(`/jig make: ${spec.path} still says \`mockup: pending\`: nobody has said whether to draw it. Ask the owner, draw it with \`/jig mockup\` or skip it; if they skip it, write \`mockup: skipped \u2014 "<their words>"\` and build from the spec alone.`);
8172
8478
  }
8173
8479
  }
8480
+ if (command === "spec" && !waiting) {
8481
+ const spec = namedSpec(root, invocation?.surface);
8482
+ if (spec && isConfirmed(spec.body) && !isConfirmed(fileAtSessionStart(root, spec.path, start))) {
8483
+ const reply = ownerReplyAfter(opts.input.transcript_path, /\bconfirm/i);
8484
+ const said = reply === void 0 ? void 0 : ownerWordProblem("confirm", reply.slice(0, 400), reply, `${spec.path}: \`confirmed: true\``);
8485
+ if (reply === void 0) problems.push(`/jig spec: ${spec.path} is recorded as confirmed, and the owner was never asked to confirm it. Put it to them with its sheet and stop; record \`confirmed: true\` when they say yes.`);
8486
+ else if (said) problems.push(`/jig spec: ${spec.path} is recorded as confirmed, but the owner's reply to being asked does not confirm it ("${reply.trim().slice(0, 120)}"). Record \`confirmed: true\` only on their yes.`);
8487
+ }
8488
+ }
8174
8489
  if (waiting && command === "spec") {
8175
- const spec = specFor(root, invocation?.surface);
8490
+ const spec = namedSpec(root, invocation?.surface);
8491
+ if (!spec && invocation?.surface && /\bconfirm/i.test(lastAssistantText(opts.input.transcript_path) ?? "")) {
8492
+ problems.push(`/jig spec: you are asking the owner to confirm a spec that is not at .jig/specs/${invocation.surface}.spec.md. Write it there, with \`confirmed: false\`, have it checked (step 3c), and ask again: the checks the owner relies on read that file, and a draft kept anywhere else is checked by nothing.`);
8493
+ }
8176
8494
  if (spec) problems.push(...specChecked(root, spec, start, owner).map((p) => `/jig spec: ${p}`));
8495
+ if (spec && existsSync27(join43(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
8496
+ problems.push(...[...specProblems(spec), ...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start))].map((p) => `/jig spec: ${p}`));
8497
+ }
8177
8498
  }
8178
8499
  const selection = selectFiles(root, false);
8179
8500
  const changedUi = selection.mode === "changed" && selection.files.some((f) => isStyleBearing(f) || isReaderText(f));
@@ -8196,11 +8517,17 @@ ${result.report}`);
8196
8517
  }
8197
8518
  }
8198
8519
  problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
8199
- const critiqueDir = join42(root, ".jig", "critique");
8200
- if (existsSync26(critiqueDir)) {
8520
+ problems.push(...probesLeftBehind(root, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
8521
+ const touched = ["AGENTS.md", "CLAUDE.md"].some((f) => {
8522
+ const now = existsSync27(join43(root, f)) ? readText(join43(root, f)) : "";
8523
+ return fileAtSessionStart(root, f, start) !== now;
8524
+ });
8525
+ if (touched) problems.push(...agentFileProblems(root).map((p) => `Jig's agent instructions: ${p}`));
8526
+ const critiqueDir = join43(root, ".jig", "critique");
8527
+ if (existsSync27(critiqueDir)) {
8201
8528
  for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
8202
- const dir = join42(critiqueDir, surface);
8203
- if (!existsSync26(join42(dir, "screen.json")) && !existsSync26(join42(dir, "code.json"))) continue;
8529
+ const dir = join43(critiqueDir, surface);
8530
+ if (!existsSync27(join43(dir, "screen.json")) && !existsSync27(join43(dir, "code.json"))) continue;
8204
8531
  const v = verifyVerdicts({ projectRoot: root, surface });
8205
8532
  if (!v.ok) {
8206
8533
  const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
@@ -8223,10 +8550,10 @@ ${problems.join("\n\n")}`
8223
8550
  }
8224
8551
  const session = opts.input.session_id ?? "unknown";
8225
8552
  const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
8226
- const stateFile = join42(root, ".jig", "gate.json");
8553
+ const stateFile = join43(root, ".jig", "gate.json");
8227
8554
  let state = {};
8228
8555
  try {
8229
- state = JSON.parse(readFileSync35(stateFile, "utf8"));
8556
+ state = JSON.parse(readFileSync36(stateFile, "utf8"));
8230
8557
  } catch {
8231
8558
  }
8232
8559
  if (problems.length === 0) {
@@ -8261,7 +8588,7 @@ function verdictDigests(dir) {
8261
8588
  for (const f of VERDICT_FILES) {
8262
8589
  let file;
8263
8590
  try {
8264
- file = JSON.parse(readFileSync35(join42(dir, f), "utf8"));
8591
+ file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
8265
8592
  } catch {
8266
8593
  continue;
8267
8594
  }
@@ -8276,7 +8603,7 @@ function writeLock(root, surface, lockPath, dir, now) {
8276
8603
  const pageFile2 = recordedPage(root, surface);
8277
8604
  let page;
8278
8605
  try {
8279
- if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync35(join42(root, pageFile2), "utf8")) };
8606
+ if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync36(join43(root, pageFile2), "utf8")) };
8280
8607
  } catch {
8281
8608
  }
8282
8609
  const tweakAt = readTweak(dir)?.at;
@@ -8289,7 +8616,7 @@ function verdictChecksum(dir) {
8289
8616
  const parts = VERDICT_FILES.map((f) => {
8290
8617
  try {
8291
8618
  return `${f}
8292
- ${readFileSync35(join42(dir, f), "utf8")}`;
8619
+ ${readFileSync36(join43(dir, f), "utf8")}`;
8293
8620
  } catch {
8294
8621
  return "";
8295
8622
  }
@@ -8297,15 +8624,15 @@ ${readFileSync35(join42(dir, f), "utf8")}`;
8297
8624
  return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
8298
8625
  }
8299
8626
  function verdictGuard(root, command, inPlay) {
8300
- const critiqueDir = join42(root, ".jig", "critique");
8301
- if (!existsSync26(critiqueDir)) return [];
8627
+ const critiqueDir = join43(root, ".jig", "critique");
8628
+ if (!existsSync27(critiqueDir)) return [];
8302
8629
  const problems = [];
8303
8630
  for (const surface of readdirSync16(critiqueDir)) {
8304
8631
  if (surface.startsWith("_")) continue;
8305
- const dir = join42(critiqueDir, surface);
8632
+ const dir = join43(critiqueDir, surface);
8306
8633
  const now = verdictChecksum(dir);
8307
8634
  if (!now) continue;
8308
- const lockPath = join42(dir, LOCK);
8635
+ const lockPath = join43(dir, LOCK);
8309
8636
  if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
8310
8637
  writeLock(root, surface, lockPath, dir, now);
8311
8638
  problems.push(...lockLeftBehind(root, surface));
@@ -8313,7 +8640,7 @@ function verdictGuard(root, command, inPlay) {
8313
8640
  }
8314
8641
  let lock = {};
8315
8642
  try {
8316
- lock = JSON.parse(readFileSync35(lockPath, "utf8"));
8643
+ lock = JSON.parse(readFileSync36(lockPath, "utf8"));
8317
8644
  } catch {
8318
8645
  continue;
8319
8646
  }
@@ -8346,6 +8673,25 @@ function lockLeftBehind(root, surface) {
8346
8673
  }
8347
8674
  return [`${rel}/${LOCK} was written when you stopped, after the verdicts it records were committed. Commit it on its own (\`git add ${rel}/${LOCK} && git commit -m "chore: record ${surface}'s verdict lock"\`), and nothing else.`];
8348
8675
  }
8676
+ function probesLeftBehind(root, surfaces) {
8677
+ if (!surfaces.length) return [];
8678
+ let lines;
8679
+ try {
8680
+ lines = execFileSync4("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
8681
+ } catch {
8682
+ return [];
8683
+ }
8684
+ const path = (l) => l.slice(3).replace(/^"|"$/g, "");
8685
+ const isProbe = (p) => /^\.jig\/critique\/[^/]+\/probe-[^/]+\.json$/.test(p);
8686
+ const isLock = (p) => /^\.jig\/critique\/[^/]+\/verdicts\.lock$/.test(p);
8687
+ if (lines.some((l) => !isProbe(path(l)) && !isLock(path(l)))) return [];
8688
+ const left = lines.map(path).filter((p) => isProbe(p) && surfaces.some((s) => p.startsWith(`.jig/critique/${s}/`)));
8689
+ if (!left.length) return [];
8690
+ const bySurface = [...new Set(left.map((p) => p.split("/")[2]))];
8691
+ return [
8692
+ `The gate re-took ${left.length} probe file${left.length === 1 ? "" : "s"} of ${bySurface.join(", ")} when you stopped, after your last commit: the page changed since they were taken. Commit them on their own (\`git add ${bySurface.map((s) => `.jig/critique/${s}/probe-*.json`).join(" ")} && git commit -m "chore: re-take ${bySurface.join(", ")}'s probes"\`), and nothing else.`
8693
+ ];
8694
+ }
8349
8695
  function tweakVerdictProblems(dir, surface, lockedVerdicts) {
8350
8696
  const record = readTweak(dir);
8351
8697
  if (!record) {
@@ -8370,7 +8716,7 @@ function unjudgedTweakIds(dir, record) {
8370
8716
  for (const f of VERDICT_FILES) {
8371
8717
  let file;
8372
8718
  try {
8373
- file = JSON.parse(readFileSync35(join42(dir, f), "utf8"));
8719
+ file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
8374
8720
  } catch {
8375
8721
  continue;
8376
8722
  }
@@ -8382,7 +8728,7 @@ function unjudgedTweakIds(dir, record) {
8382
8728
  }
8383
8729
  function save(file, state) {
8384
8730
  try {
8385
- mkdirSync8(join42(file, ".."), { recursive: true });
8731
+ mkdirSync8(join43(file, ".."), { recursive: true });
8386
8732
  writeFileSync6(file, JSON.stringify(state), "utf8");
8387
8733
  } catch {
8388
8734
  }
@@ -8390,7 +8736,7 @@ function save(file, state) {
8390
8736
 
8391
8737
  // src/index.ts
8392
8738
  var packageRoot = getPackageRoot();
8393
- var { version } = JSON.parse(readFileSync36(join43(packageRoot, "package.json"), "utf8"));
8739
+ var { version } = JSON.parse(readFileSync37(join44(packageRoot, "package.json"), "utf8"));
8394
8740
  function warnIfUnpublishedPin() {
8395
8741
  if (isDevVersion(version)) {
8396
8742
  console.warn(
@@ -8434,7 +8780,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
8434
8780
  projectRoot,
8435
8781
  packageRoot: assetRoot(),
8436
8782
  version,
8437
- homeDir: homedir2(),
8783
+ homeDir: homedir3(),
8438
8784
  hook
8439
8785
  });
8440
8786
  if (result.warning) {
@@ -8443,6 +8789,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
8443
8789
  }
8444
8790
  if (result.hookOnly) {
8445
8791
  console.log(result.hookOnly);
8792
+ for (const f of result.agentFiles ?? []) console.log(` + ${f} (Jig's block for your agent; your own text in it is untouched)`);
8446
8793
  if (result.stopHook === true) console.log(" + .claude/settings.json (Stop hook: jig gate blocks finishing while check or a critique fails)");
8447
8794
  if (result.stopHook === false) console.log(" ! .claude/settings.json is not valid JSON \u2014 the Stop hook was not added. Fix the file and run install again.");
8448
8795
  return;
@@ -8451,6 +8798,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
8451
8798
  warnIfUnpublishedPin();
8452
8799
  for (const f of result.written) console.log(` + ${f}`);
8453
8800
  for (const f of result.skipped) console.log(` \xB7 ${f} (edited locally, left alone)`);
8801
+ for (const f of result.agentFiles ?? []) console.log(` + ${f} (Jig's block for your agent; your own text in it is untouched)`);
8454
8802
  if (result.stopHook === true) console.log(" + .claude/settings.json (Stop hook: jig gate blocks finishing while check or a critique fails)");
8455
8803
  if (result.stopHook === false) console.log(" ! .claude/settings.json is not valid JSON \u2014 the Stop hook was not added. Fix the file and run install again.");
8456
8804
  if (result.stopHook === void 0 && opts.agent === "claude" && opts.scope === "project") {
@@ -8478,7 +8826,7 @@ program.command("update").description("Update vendored Jig rules, skipping files
8478
8826
  projectRoot,
8479
8827
  packageRoot: assetRoot(),
8480
8828
  version,
8481
- homeDir: homedir2()
8829
+ homeDir: homedir3()
8482
8830
  });
8483
8831
  const label = result.targets.map((t) => `${t.agent} (${t.scope}, ${t.fromVersion})`).join(", ");
8484
8832
  warnIfUnpublishedPin();
@@ -8497,6 +8845,16 @@ program.command("update").description("Update vendored Jig rules, skipping files
8497
8845
  process.exit(1);
8498
8846
  }
8499
8847
  });
8848
+ program.command("checksum").description("Print a spec's checksum, the value its .checked.json records as `spec`.").argument("<spec>", "the spec by name (tokens-and-modes) or path (.jig/specs/tokens-and-modes.spec.md)").action((name) => {
8849
+ const projectRoot = findProjectRoot(process.cwd());
8850
+ const slug = name.replace(/^.*\//, "").replace(/\.spec\.md$/, "");
8851
+ const path = join44(projectRoot, ".jig", "specs", `${slug}.spec.md`);
8852
+ if (!existsSync28(path)) {
8853
+ console.error(` \u2717 no spec at .jig/specs/${slug}.spec.md`);
8854
+ process.exit(1);
8855
+ }
8856
+ console.log(specChecksum(readFileSync37(path, "utf8")));
8857
+ });
8500
8858
  program.command("verdicts").description("Verify a critique's verdict files and compute its counts.").argument("<surface>", "the surface slug the critique wrote under .jig/critique/").option("--reprobe", "re-take every probe that is missing or was taken on an older version of the page, first (needs a browser)").action(async (surface, opts) => {
8501
8859
  const projectRoot = findProjectRoot(process.cwd());
8502
8860
  try {
@@ -8541,7 +8899,7 @@ program.command("check").description("Check the repo against Jig's mechanical +
8541
8899
  try {
8542
8900
  const result = check({
8543
8901
  projectRoot,
8544
- homeDir: homedir2(),
8902
+ homeDir: homedir3(),
8545
8903
  version,
8546
8904
  all: opts.all,
8547
8905
  ci: opts.ci
@@ -8624,13 +8982,14 @@ program.command("probe").description("Print the render probe. With --save, read
8624
8982
  program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
8625
8983
  let input = {};
8626
8984
  try {
8627
- if (!process.stdin.isTTY) input = JSON.parse(readFileSync36(0, "utf8") || "{}");
8985
+ if (!process.stdin.isTTY) input = JSON.parse(readFileSync37(0, "utf8") || "{}");
8628
8986
  } catch {
8629
8987
  }
8630
8988
  const cwd = input.cwd ?? process.cwd();
8631
8989
  try {
8632
8990
  const projectRoot = findProjectRoot(cwd);
8633
- const critiqued = critiquedSurfaces(projectRoot);
8991
+ const byHand = !input.transcript_path;
8992
+ const critiqued = byHand ? [] : critiquedSurfaces(projectRoot);
8634
8993
  for (const surface of surfacesToProbe(projectRoot, input).filter((s) => critiqued.includes(s))) {
8635
8994
  const page = surfacePage(projectRoot, surface) ?? recordedPage(projectRoot, surface);
8636
8995
  if (!page) continue;
@@ -8653,7 +9012,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
8653
9012
  await init({
8654
9013
  projectRoot,
8655
9014
  packageRoot: assetRoot(),
8656
- homeDir: homedir2(),
9015
+ homeDir: homedir3(),
8657
9016
  version,
8658
9017
  yes: opts.yes
8659
9018
  });
@@ -8664,7 +9023,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
8664
9023
  });
8665
9024
  function resolveSurfaceMode(projectRoot) {
8666
9025
  try {
8667
- const config = JSON.parse(readFileSync36(join43(projectRoot, "jig.config.json"), "utf8"));
9026
+ const config = JSON.parse(readFileSync37(join44(projectRoot, "jig.config.json"), "utf8"));
8668
9027
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
8669
9028
  return modes.length === 1 ? modes[0] : void 0;
8670
9029
  } catch {